diff --git a/.claude/hooks/post-edit.sh b/.claude/hooks/post-edit.sh index 5c4cc766db..c5c04ef2a1 100755 --- a/.claude/hooks/post-edit.sh +++ b/.claude/hooks/post-edit.sh @@ -2,21 +2,26 @@ # # PostToolUse hook (Edit|Write|MultiEdit) for Meshtastic-Android. # -# Front-runs three of this repo's own CI/governance gates locally, so the +# Front-runs four of this repo's own CI/governance gates locally, so the # failure surfaces at edit time instead of in CI. Dispatches by edited path: # # - base strings.xml -> run scripts/sort-strings.py (keeps the file sorted # and regenerates .skills/compose-ui/strings-index.txt; # AGENTS.md mandates this but no CI job enforces it) -# - fastlane/metadata/** -> run scripts/check-metadata-length.py and BLOCK on -# overlength store listings (the pull-request.yml +# - fastlane/metadata/** -> run scripts/check-store-metadata.py and BLOCK on +# store-rule violations (the pull-request.yml # check-metadata job is blocking; F-Droid #4262) -# - settings.gradle.kts -> remind about the pull-request.yml paths-filter drift -# guard for NEW top-level modules (#5735) +# - settings.gradle.kts -> remind about the pull-request.yml paths-filter and +# ALL_MODULES_FULL drift guards for NEW top-level modules +# - commonMain/commonTest -> BLOCK on java.*/android.* imports in .kt files (the +# KMP boundary, otherwise first caught by the iOS compile +# in kmpSmokeCompile for main sources or allTests for tests) +# +# Kotlin edits outside tests and previews also get warn-only Compose-pitfall notes. # # FAILS OPEN: any tooling/parse error allows the edit to stand (exit 0). Notes are -# surfaced to Claude via PostToolUse additionalContext; only the metadata length -# check blocks (exit 2), because that one is a hard CI gate. +# surfaced to Claude via PostToolUse additionalContext; only the store metadata and +# KMP-boundary checks block (exit 2), because each front-runs a failing CI job. input=$(cat) @@ -41,8 +46,7 @@ emit_context() { case "$file_path" in *core/resources/src/commonMain/composeResources/values/strings.xml) - out=$( (cd "$repo_root" && python3 scripts/sort-strings.py) 2>&1 ) - if [ $? -eq 0 ]; then + if out=$( (cd "$repo_root" && python3 scripts/sort-strings.py) 2>&1 ); then emit_context "Auto-ran scripts/sort-strings.py: base strings.xml re-sorted and .skills/compose-ui/strings-index.txt regenerated. Line positions changed — re-read the file before any further edits to it." else emit_context "Tried to auto-run scripts/sort-strings.py after your strings.xml edit but it failed (likely malformed XML in what was just written — please check): @@ -51,11 +55,13 @@ $out" ;; *fastlane/metadata/android/*) - out=$( (cd "$repo_root" && python3 scripts/check-metadata-length.py) 2>&1 ) - if [ $? -ne 0 ]; then + out=$( (cd "$repo_root" && python3 scripts/check-store-metadata.py) 2>&1 ) + rc=$? + # 1 is a store-rule violation; any other failure is tooling and fails open. + if [ "$rc" -eq 1 ]; then { - printf '%s\n' "Store-listing metadata exceeds a length limit (scripts/check-metadata-length.py)." - printf '%s\n' "Fix this before it lands — the pull-request.yml check-metadata job is blocking (F-Droid #4262; limits count Unicode code points, not bytes). Details:" + printf '%s\n' "Store-listing metadata breaks a store rule (scripts/check-store-metadata.py)." + printf '%s\n' "Fix this before it lands: the pull-request.yml check-metadata job is blocking (F-Droid #4262; limits count Unicode code points, not bytes). Details:" printf '%s\n' "$out" } >&2 exit 2 @@ -64,7 +70,7 @@ $out" ;; *settings.gradle.kts) - emit_context "You edited settings.gradle.kts. If you added a NEW TOP-LEVEL module directory, add its '/**' line to the 'android:' paths-filter in .github/workflows/pull-request.yml (case-sensitive) or the verify-check-changes-filter drift guard will fail the PR (bit us on #5735). New sub-modules under an already-listed root (core/**, feature/**, etc.) are already covered — no change needed." + emit_context "You edited settings.gradle.kts. If you added a NEW TOP-LEVEL module directory, add its '/**' line to the 'android:' paths-filter in .github/workflows/pull-request.yml (case-sensitive) or scripts/check-changes-filter.py will fail the PR, and add the module to ALL_MODULES_FULL in RootConventionPlugin.kt or scripts/check-module-list.py will. New sub-modules under an already-listed root (core/**, feature/**, etc.) need no filter change." ;; */src/commonMain/*.kt|*/src/commonTest/*.kt) diff --git a/.claude/skills/ci-cost-control b/.claude/skills/ci-cost-control new file mode 120000 index 0000000000..55ef205bab --- /dev/null +++ b/.claude/skills/ci-cost-control @@ -0,0 +1 @@ +../../.skills/ci-cost-control \ No newline at end of file diff --git a/.claude/skills/code-review b/.claude/skills/code-review new file mode 120000 index 0000000000..ab060e846a --- /dev/null +++ b/.claude/skills/code-review @@ -0,0 +1 @@ +../../.skills/code-review \ No newline at end of file diff --git a/.claude/skills/compose-ui b/.claude/skills/compose-ui new file mode 120000 index 0000000000..d3955166d5 --- /dev/null +++ b/.claude/skills/compose-ui @@ -0,0 +1 @@ +../../.skills/compose-ui \ No newline at end of file diff --git a/.claude/skills/design-standards b/.claude/skills/design-standards new file mode 120000 index 0000000000..d0e3eaf9e7 --- /dev/null +++ b/.claude/skills/design-standards @@ -0,0 +1 @@ +../../.skills/design-standards \ No newline at end of file diff --git a/.claude/skills/implement-feature b/.claude/skills/implement-feature new file mode 120000 index 0000000000..5e67aebad1 --- /dev/null +++ b/.claude/skills/implement-feature @@ -0,0 +1 @@ +../../.skills/implement-feature \ No newline at end of file diff --git a/.claude/skills/kmp-architecture b/.claude/skills/kmp-architecture new file mode 120000 index 0000000000..cf832c6459 --- /dev/null +++ b/.claude/skills/kmp-architecture @@ -0,0 +1 @@ +../../.skills/kmp-architecture \ No newline at end of file diff --git a/.claude/skills/navigation-and-di b/.claude/skills/navigation-and-di new file mode 120000 index 0000000000..ad321cf61f --- /dev/null +++ b/.claude/skills/navigation-and-di @@ -0,0 +1 @@ +../../.skills/navigation-and-di \ No newline at end of file diff --git a/.claude/skills/new-branch b/.claude/skills/new-branch new file mode 120000 index 0000000000..f45be4c0e8 --- /dev/null +++ b/.claude/skills/new-branch @@ -0,0 +1 @@ +../../.skills/new-branch \ No newline at end of file diff --git a/.claude/skills/project-overview b/.claude/skills/project-overview new file mode 120000 index 0000000000..9b2fcc3f98 --- /dev/null +++ b/.claude/skills/project-overview @@ -0,0 +1 @@ +../../.skills/project-overview \ No newline at end of file diff --git a/.claude/skills/run-meshtastic-android/SKILL.md b/.claude/skills/run-meshtastic-android/SKILL.md index 35c41a0c23..cbb4748d20 100644 --- a/.claude/skills/run-meshtastic-android/SKILL.md +++ b/.claude/skills/run-meshtastic-android/SKILL.md @@ -123,11 +123,14 @@ python3 .claude/skills/run-meshtastic-android/driver_emulator.py -s emulator-555 connect=t10.0.2.2:4404 wait_text=RPLY ss=/tmp/emu.png ``` -`connect` force-stops the app, relaunches `org.meshtastic.app.MainActivity` with the -debug-only `skip_onboarding` extra and the `/connections?address=` deeplink -(`t` = TCP, `x` = BLE, `s` = serial, `n` = disconnect — full path list in -`docs/en/developer/navigation-and-deep-links.md`), then waits for the trust dialog -newer builds pop and taps its **Connect** button. Success looks like the Connection +`connect` force-stops the app, relaunches it through `org.meshtastic.app.AutomationLauncher` +with the `skip_onboarding` and `skip_connect_confirm` extras and the +`/connections?address=` deeplink (`t` = TCP, `x` = BLE, `s` = serial, `n` = disconnect — +full path list in `docs/en/developer/navigation-and-deep-links.md`). That alias exists +only in debug builds and only the shell may start it, and the extras do nothing on any +other launch. With `skip_connect_confirm` the address is applied with no trust dialog; a +build that predates the alias still pops it, and the driver taps its **Connect** button. Desktop +takes the same switch as a `--skip-connect-confirm` argument in a non-release build. Success looks like the Connection screen showing `RPLY Replay Observer` with a **Disconnect** button, and `replay_status` reporting `connected:true`. diff --git a/.claude/skills/run-meshtastic-android/driver_emulator.py b/.claude/skills/run-meshtastic-android/driver_emulator.py index 6802eae681..bedf53ab01 100644 --- a/.claude/skills/run-meshtastic-android/driver_emulator.py +++ b/.claude/skills/run-meshtastic-android/driver_emulator.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 """Drive the Meshtastic Android app on an emulator/device over adb. -Scripted bring-up (never hand-walk onboarding): launches MainActivity with the -debug-only skip_onboarding extra and a /connections deeplink that auto-connects -to a TCP radio — pair it with a replay-sim radio (an AVD reaches the host at +Scripted bring-up (never hand-walk onboarding): launches the debug build's +shell-only AutomationLauncher alias with the skip_onboarding extra and a +/connections deeplink that auto-connects to a TCP radio — pair it with a +replay-sim radio (an AVD reaches the host at 10.0.2.2). Handles the trust dialog newer builds pop on first connect. Usage: @@ -38,18 +39,30 @@ import xml.etree.ElementTree as ET SERIAL = None PKG = "com.geeksville.mesh.fdroid.debug" -ACTIVITY = "org.meshtastic.app.MainActivity" +ACTIVITY = "org.meshtastic.app.AutomationLauncher" +# Builds without the alias; they ignore the launch switches. +FALLBACK_ACTIVITY = "org.meshtastic.app.MainActivity" def adb(*args): cmd = ["adb"] + (["-s", SERIAL] if SERIAL else []) + list(args) r = subprocess.run(cmd, capture_output=True, timeout=120) if r.returncode != 0: - err = (r.stderr or b"").decode(errors="replace").strip() + # am start reports a missing component on stdout + err = ((r.stderr or b"") + (r.stdout or b"")).decode(errors="replace").strip() raise RuntimeError(f"adb {' '.join(args)} failed ({r.returncode}): {err[:300]}") return (r.stdout or b"").decode(errors="replace") +def start_app(*extras): + try: + return adb("shell", "am", "start", "-n", f"{PKG}/{ACTIVITY}", *extras) + except RuntimeError as e: + if "does not exist" not in str(e): + raise + return adb("shell", "am", "start", "-n", f"{PKG}/{FALLBACK_ACTIVITY}", *extras) + + def ui_dump(): adb("shell", "uiautomator", "dump", "/sdcard/ui.xml") return adb("shell", "cat", "/sdcard/ui.xml") @@ -118,19 +131,26 @@ def wait_text(text, timeout=60): def connect(addr): adb("shell", "am", "force-stop", PKG) time.sleep(1) - adb( - "shell", "am", "start", "-n", f"{PKG}/{ACTIVITY}", + start_app( "--ez", "skip_onboarding", "true", + "--ez", "skip_connect_confirm", "true", "-a", "android.intent.action.VIEW", "-d", f"https://meshtastic.org/connections?address={addr}", ) - # Builds >2.8.1 pop a trust dialog on first connect to a new device. Match its - # title, not bare "Connect" — that substring also matches "Stop Connecting". - r = wait_text("Connect to this device", timeout=30) - if r.startswith("found"): - print(tap_text("Connect")) + # Debug builds with the AutomationLauncher alias apply the address with no dialog; + # older ones pop the trust dialog, sometimes late on a slow emulator. + # Watch for either for 30 s. Match the dialog's title, not bare "Connect", which + # also matches "Stop Connecting". + deadline = time.time() + 30 + while time.time() < deadline: + if any(True for _ in find("Disconnect")): + break + if any(True for _ in find("Connect to this device")): + print(tap_text("Connect")) + break + time.sleep(3) else: - print("no trust dialog seen — verifying the connection directly") + print("neither the trust dialog nor a connection appeared in 30 s") # A missing dialog does not prove success (the launch or deeplink may have failed): # require the Connection screen's Disconnect button before claiming victory. v = wait_text("Disconnect", timeout=60) @@ -162,7 +182,7 @@ def main(): if res.startswith("FAILED"): return 1 elif name == "launch": - adb("shell", "am", "start", "-n", f"{PKG}/{ACTIVITY}", "--ez", "skip_onboarding", "true") + start_app("--ez", "skip_onboarding", "true") print("launched") elif name == "stop": adb("shell", "am", "force-stop", PKG) diff --git a/.claude/skills/speckit b/.claude/skills/speckit new file mode 120000 index 0000000000..a18c872c8e --- /dev/null +++ b/.claude/skills/speckit @@ -0,0 +1 @@ +../../.skills/speckit \ No newline at end of file diff --git a/.claude/skills/testing-ci b/.claude/skills/testing-ci new file mode 120000 index 0000000000..294acdeccd --- /dev/null +++ b/.claude/skills/testing-ci @@ -0,0 +1 @@ +../../.skills/testing-ci \ No newline at end of file diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 6889357ee8..7b7dcb8b16 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -8,6 +8,16 @@ reviews: profile: chill high_level_summary: true poem: false + # Keep the walkthrough to the summary and the changed files. review_status stays on: + # tooling reads the paused, draft and rate-limited notices from it. + sequence_diagrams: false + related_prs: false + related_issues: false + suggested_labels: false + suggested_reviewers: false + estimate_code_review_effort: false + in_progress_fortune: false + enable_prompt_for_ai_agents: false # Don't burn reviews on WIP. This repo opens lots of draft PRs; review on "ready". # Skip Renovate dependency updates — CI gates dependencies; we review for substance, not every bump. auto_review: @@ -146,7 +156,7 @@ reviews: instructions: > New string resources must be alphabetically sorted (scripts/sort-strings.py). Flag out-of-order additions. - path: baselineprofile/ - instructions: Keep baseline profile generation tied to the `google` flavor and connected devices/emulators, and commit the generated profile output to `androidApp/src/googleRelease/generated/baselineProfiles/baseline-prof.txt`. + instructions: Keep baseline profile generation tied to the `google` flavor and connected devices/emulators, and commit the generated profile output to `androidApp/src/main/generated/baselineProfiles/baseline-prof.txt`, which both flavors ship. - path: docs/ instructions: Treat non-English locale folders as Crowdin-managed output; edit the English sources under `docs/en/` and register new pages through `feature/docs/` instead of hand-editing translated locale directories. - path: screenshot-tests/ @@ -324,6 +334,11 @@ reviews: good opportunity to fix during this refactor" so the author can decide on scope. Do not flag a move that the diff shows to be genuinely mechanical. +chat: + # Answer only when tagged, so a plain reply on a thread does not draw a review of its own. + # Tag @coderabbitai on a decline whose reason should become a learning. + auto_reply: false + knowledge_base: # Learnings are how a confirmed finding stops recurring on the next PR. Pin the # scope to this repo: the default `auto` already resolves to `local` for public diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ef57ec56dc..1741f144b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,7 +2,7 @@ name: Bug Report description: File a bug report. title: "[Bug]: " labels: [bug] -projects: [meshtastic/Meshtastic-Android] +projects: [meshtastic/26] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 1c7881533d..9e69fdba6c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,7 +2,7 @@ name: Feature Request description: File a request for new feature or functionality. title: "[Feature Request]: " labels: [enhancement] -projects: [meshtastic/Meshtastic-Android] +projects: [meshtastic/30] body: - type: checkboxes id: checklist diff --git a/.github/ISSUE_TEMPLATE/zbug_report_internal.yml b/.github/ISSUE_TEMPLATE/zbug_report_internal.yml index 5f01a45734..534aee2321 100644 --- a/.github/ISSUE_TEMPLATE/zbug_report_internal.yml +++ b/.github/ISSUE_TEMPLATE/zbug_report_internal.yml @@ -2,7 +2,7 @@ name: Internal testing - Bug Report description: File a bug report. title: "[Bug]: " labels: [bug, ch_testing] -projects: [meshtastic/Meshtastic-Android] +projects: [meshtastic/26] body: - type: markdown attributes: diff --git a/.github/actions/bot-pr/action.yml b/.github/actions/bot-pr/action.yml new file mode 100644 index 0000000000..038f5f1fb4 --- /dev/null +++ b/.github/actions/bot-pr/action.yml @@ -0,0 +1,78 @@ +name: Bot PR +description: Open or update a PR from the working tree's changes and hand it to the merge queue. +# The token must be a PAT: GITHUB_TOKEN may not enable auto-merge on protected main, and +# PRs it opens start no workflows, so the required checks would never report. +inputs: + token: + description: 'PAT that pushes the branch, opens the PR and requests the merge' + required: true + branch: + description: 'PR head branch' + required: true + base: + description: 'PR base branch; must be the branch checked out in the workspace' + default: 'main' + title: + description: 'PR title' + required: true + commit-message: + description: 'Commit message; defaults to the title' + default: '' + body: + description: 'PR body' + default: '' + add-paths: + description: 'Newline-separated pathspecs to commit; all changes when empty' + default: '' + labels: + description: 'Newline-separated labels' + default: '' +outputs: + number: + description: 'PR number' + value: ${{ steps.pr.outputs.pull-request-number }} + url: + description: 'PR URL' + value: ${{ steps.pr.outputs.pull-request-url }} + operation: + description: 'created, updated, closed or none' + value: ${{ steps.pr.outputs.pull-request-operation }} +runs: + using: composite + steps: + - name: Open or update the PR + id: pr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ inputs.token }} + branch: ${{ inputs.branch }} + base: ${{ inputs.base }} + title: ${{ inputs.title }} + commit-message: ${{ inputs.commit-message || inputs.title }} + body: ${{ inputs.body }} + add-paths: ${{ inputs.add-paths }} + labels: ${{ inputs.labels }} + delete-branch: true + + # No merge method is passed; the queue sets it. Re-requested on every update, since a + # push can clear the request. GitHub refuses it on a PR that is already mergeable, so + # that case merges directly. + - name: Enable auto-merge + if: ${{ steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' }} + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }} + run: | + PR_ID=$(gh pr view "$PR_NUMBER" --json id --jq .id) + QUERY=$(cat <<'GQL' + mutation($id: ID!) { + enablePullRequestAutoMerge(input: { pullRequestId: $id }) { + pullRequest { number autoMergeRequest { enabledAt } } + } + } + GQL + ) + gh api graphql -f query="$QUERY" -F id="$PR_ID" \ + || gh pr merge "$PR_NUMBER" diff --git a/.github/actions/gradle-setup/action.yml b/.github/actions/gradle-setup/action.yml index c685e880c5..b8fec77312 100644 --- a/.github/actions/gradle-setup/action.yml +++ b/.github/actions/gradle-setup/action.yml @@ -2,13 +2,22 @@ name: Gradle Setup description: Setup Java and Gradle for KMP builds inputs: cache_read_only: - description: 'Whether Gradle cache is read-only' + description: 'Whether the Gradle, Kotlin/Native and Robolectric caches are read-only' + default: 'true' + cache_konan: + description: 'Cache ~/.konan. Only jobs that run Kotlin/Native tasks read it.' + default: 'false' + cache_robolectric: + description: 'Cache the Robolectric android-all jars in ~/.m2' default: 'true' jdk_distribution: description: 'JDK distribution (temurin or jetbrains)' default: 'temurin' install_jetbrains_jdk: - description: 'Also install JetBrains JDK 25 for Compose Desktop toolchain resolution' + description: | + Also install JetBrains JDK 25 with setup-java. Gradle detects it through ~/.m2/toolchains.xml + instead of provisioning one through foojay, so the Linux release installers bundle this JBR + and flatpak-sources records no foojay download as a source. default: 'false' gradle_encryption_key: description: 'Encryption key for Gradle remote cache' @@ -24,20 +33,8 @@ inputs: dependency_graph: description: | Dependency graph mode: disabled | generate | generate-and-submit | generate-and-upload | - download-and-submit. Submit needs `contents: write`. Never combine with - cache_configuration_cache — a CC-hit build generates NO graph. + download-and-submit. Submit needs `contents: write`. default: 'disabled' - cache_configuration_cache: - description: | - Persist .gradle/configuration-cache. Opt-in: only pays off when config inputs are - commit-stable (the VERSION_CODE-pinned jobs). Real-versionCode jobs would restore, - miss, and never re-save. - default: 'false' - cache_key_suffix: - description: | - Extra CC-key discriminator for matrix legs running different task graphs (test-shards). - Runner-only matrices don't need it — os/arch are already in the key. - default: '' runs: using: composite steps: @@ -45,9 +42,6 @@ runs: shell: bash run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - - name: Set up JDK 25 uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: @@ -55,16 +49,8 @@ runs: distribution: ${{ inputs.jdk_distribution }} token: ${{ github.token }} - - name: Restore cached JetBrains JDK 25 + - name: Set up JetBrains JDK 25 if: inputs.install_jetbrains_jdk == 'true' - id: cache-jbr - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: ${{ runner.tool_cache }}/Java_JetBrains_jdk - key: jbr-25-${{ runner.os }}-${{ runner.arch }} - - - name: Set up JetBrains JDK 25 (for Compose Desktop) - if: inputs.install_jetbrains_jdk == 'true' && steps.cache-jbr.outputs.cache-hit != 'true' id: setup-jbr continue-on-error: true uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 @@ -74,32 +60,66 @@ runs: check-latest: false token: ${{ github.token }} - - name: JBR setup skipped or failed — Gradle will auto-provision via Foojay - if: inputs.install_jetbrains_jdk == 'true' && steps.cache-jbr.outputs.cache-hit != 'true' && steps.setup-jbr.outcome == 'failure' + - name: JBR setup failed, Gradle will provision it through foojay + if: inputs.install_jetbrains_jdk == 'true' && steps.setup-jbr.outcome == 'failure' shell: bash run: echo "::warning::JBR setup-java failed (likely GitHub API rate limit). Gradle will auto-provision JBR via Foojay toolchain resolver." - # Kotlin/Native lives in ~/.konan, OUTSIDE the Gradle home that - # setup-gradle caches — without this every job re-downloads the K/N - # toolchain (downloadKotlinNativeDistribution) and the iOS-target compile - # tasks it feeds miss the build cache, while the identical tasks hit - # FROM-CACHE locally. Keyed on the version catalog: over-invalidates on - # unrelated bumps, but konan re-downloads are exactly what it prevents. - - name: Cache Kotlin/Native toolchain - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + # ~/.konan and ~/.m2 sit outside the Gradle User Home, so setup-gradle does not cache them. + # A save step here would run before ./gradlew, so writers rely on actions/cache's end-of-job save. + - name: Read the Kotlin version + if: inputs.cache_konan == 'true' + id: kotlin + shell: bash + run: | + version=$(awk -F'"' '/^kotlin *= *"/ { print $2; exit }' gradle/libs.versions.toml) + if [ -z "$version" ]; then + echo "::error file=gradle/libs.versions.toml::no kotlin version entry" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Restore the Kotlin/Native toolchain + if: inputs.cache_konan == 'true' && inputs.cache_read_only == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.konan - key: konan-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('gradle/libs.versions.toml') }} + key: konan-${{ runner.os }}-${{ runner.arch }}-${{ steps.kotlin.outputs.version }} + restore-keys: | + konan-${{ runner.os }}-${{ runner.arch }}- - # Robolectric downloads its android-all-instrumented jars to the local Maven - # repo, which lives outside the Gradle User Home and so isn't covered by - # setup-gradle's caching. (This was previously attempted via - # gradle-home-cache-includes, but those entries resolve relative to the - # Gradle User Home — the `~/.m2/...` line expanded to the literal path - # `/~/.m2/...` and never matched anything.) Keyed on the - # version catalog, which pins the Robolectric version. - - name: Cache Robolectric android-all jars - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + - name: Cache the Kotlin/Native toolchain + if: inputs.cache_konan == 'true' && inputs.cache_read_only != 'true' + id: konan + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ runner.arch }}-${{ steps.kotlin.outputs.version }} + restore-keys: | + konan-${{ runner.os }}-${{ runner.arch }}- + + # A restore-keys match brings the previous Kotlin's bundle along; without this the saved entry grows every bump. + - name: Drop other Kotlin/Native bundles before the save + if: inputs.cache_konan == 'true' && inputs.cache_read_only != 'true' && steps.konan.outputs.cache-hit != 'true' + shell: bash + env: + KOTLIN_VERSION: ${{ steps.kotlin.outputs.version }} + run: | + [ -d ~/.konan ] || exit 0 + find ~/.konan -mindepth 1 -maxdepth 1 -name 'kotlin-native-prebuilt-*' ! -name "kotlin-native-prebuilt-*-${KOTLIN_VERSION}" -print -exec rm -rf {} + + + - name: Restore the Robolectric android-all jars + if: inputs.cache_robolectric == 'true' && inputs.cache_read_only == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.m2/repository/org/robolectric + key: robolectric-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('gradle/libs.versions.toml') }} + restore-keys: | + robolectric-${{ runner.os }}-${{ runner.arch }}- + + - name: Cache the Robolectric android-all jars + if: inputs.cache_robolectric == 'true' && inputs.cache_read_only != 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.m2/repository/org/robolectric key: robolectric-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('gradle/libs.versions.toml') }} @@ -107,7 +127,7 @@ runs: robolectric-${{ runner.os }}-${{ runner.arch }}- - name: Setup Gradle - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6 with: cache-read-only: ${{ inputs.cache_read_only }} cache-encryption-key: ${{ inputs.gradle_encryption_key }} @@ -140,21 +160,3 @@ runs: # cache latency turns out to cost more than the restore it replaced. gradle-home-cache-excludes: | caches/build-cache-1 - - # CC entries live in the project dir; setup-gradle only caches the Gradle User Home — - # Develocity measured 100% CC miss (~52s config/build) before this. Runs after Setup - # Gradle so GRADLE_ENCRYPTION_KEY is exported. No sha in the key: unchanged build files - # hit exactly and skip the save; restore-keys is the self-heal for stale entries. - # The wrapper hash is its own restore-key segment: a stale same-version entry is a - # graceful CC miss, but an entry written by a DIFFERENT Gradle version can crash - # fingerprint deserialization outright (seen on 9.6.1 entries under 9.7.0). - - name: Cache Gradle configuration-cache - # Gates: opted in; key present (undecryptable otherwise — keeps keyless fork PRs off - # these entries); not merge_group (throwaway cache scope). - if: inputs.cache_configuration_cache == 'true' && inputs.gradle_encryption_key != '' && github.event_name != 'merge_group' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: ${{ github.workspace }}/.gradle/configuration-cache - key: gradle-cc-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}${{ inputs.cache_key_suffix && format('-{0}', inputs.cache_key_suffix) || '' }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'build-logic/**', 'gradle/*.gradle', 'gradle/libs.versions.toml', 'gradle.properties', 'config.properties', '.github/ci-gradle.properties') }} - restore-keys: | - gradle-cc-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}${{ inputs.cache_key_suffix && format('-{0}', inputs.cache_key_suffix) || '' }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}- diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index 0a206f5188..bc78dce3df 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -12,13 +12,17 @@ org.gradle.daemon=false # ── Memory ──────────────────────────────────────────────────────────── -# Public-repo ubuntu-24.04 runners have 16 GB RAM. Keep Gradle + Kotlin daemon -# within budget (4g Gradle + 6g Kotlin daemon, leaving room for lint and K/N). +# Public-repo hosted Ubuntu x64 runners have 16 GB RAM; each -Xmx is a ceiling, not a reservation. # Only kotlin.daemon.jvmargs is read from a properties file; kotlin.daemon.jvm.options -# is a system property. Unset, the daemon inherits org.gradle.jvmargs' 4g and OOMs. -org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 +# is a system property. Unset, the daemon inherits org.gradle.jvmargs and OOMs. +org.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 kotlin.daemon.jvmargs=-Xmx6g -XX:+UseG1GC +# ── Kotlin/Native ───────────────────────────────────────────────────── +# Each K/N compile gets its own JVM with gradle.properties' kotlin.native.jvmArgs heap. In process, +# koin-compiler-plugin 1.2.1 keeps every compile's IR reachable from the Gradle daemon until the build ends. +kotlin.native.disableCompilerDaemon=true + # ── Parallelism ─────────────────────────────────────────────────────── org.gradle.parallel=true org.gradle.workers.max=4 @@ -44,9 +48,6 @@ ksp.incremental=false # ── Android ────────────────────────────────────────────────────────── android.experimental.lint.analysisPerComponent=true -# Disable unused build features to reduce build time -android.defaults.buildfeatures.resvalues=false -android.defaults.buildfeatures.shaders=false # ── Misc ───────────────────────────────────────────────────────────── org.gradle.welcome=never diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2de14b2398..93435524a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,14 +8,14 @@ Requires JDK 25 and `ANDROID_HOME`. No secrets step: the secrets plugin reads `secrets.properties` and falls back to the tracked `secrets.defaults.properties`, so the `google` flavor builds without either. Add `secrets.properties` only for real Maps tiles. ```bash -./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests # full local verification (run before push) +./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests # full local verification (run before push) ./gradlew :core:data:allTests # single KMP module ./gradlew :androidApp:testFdroidDebugUnitTest # single Android-only module ./gradlew kmpSmokeCompile # cross-platform compile check, no tests ``` > Both `test` AND `allTests` are needed — `allTests` covers KMP modules, `test` covers pure-Android modules. -**KMP vs Android-only task naming** (wrong name silently skips tests or fails resolution): KMP modules (`core:*`, `feature:*`) use `:module:allTests` and `:module:compileKotlinJvm`; Android-only modules (`androidApp`, `desktopApp`, `core:barcode`) use `:module:testFdroidDebugUnitTest` (plain `:desktopApp:test` for the JVM-only desktop module). `:module:detekt` is the lifecycle task for both — never `detektMain`/`detektDebug`. Full matrix and pitfalls: `.skills/testing-ci/`. +**KMP vs Android-only task naming** (wrong name silently skips tests or fails resolution): KMP modules (`core:*`, `feature:*`) use `:module:allTests` and `:module:compileKotlinJvm`; Android-only modules (`androidApp`, `desktopApp`, `core:barcode`) use `:module:testFdroidDebugUnitTest` (plain `:desktopApp:test` for the JVM-only desktop module). `:module:detekt` runs the rules that need no classpath and `:module:detektTypeResolved` the ones that do (both kinds of module); never call `detektMain`/`detektDebug` directly. Full matrix and pitfalls: `.skills/testing-ci/`. Architecture, flavors, conventions, branch naming, protos, coding rules: **see `AGENTS.md`**. Contextual `.github/instructions/` files enforce conventions scoped to specific source sets. diff --git a/.github/instructions/build-logic.instructions.md b/.github/instructions/build-logic.instructions.md index 548bf6fd57..66a1ade4f8 100644 --- a/.github/instructions/build-logic.instructions.md +++ b/.github/instructions/build-logic.instructions.md @@ -7,4 +7,4 @@ applyTo: "build-logic/**/*.kt" - Prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). - Avoid `afterEvaluate` unless there is no viable lazy alternative. - Check `gradle/libs.versions.toml` for version catalog aliases before adding new ones. -- 24 convention plugin ids are registered in `build-logic/convention/build.gradle.kts` — read that block rather than guessing. The ones module builds apply most often: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.library.compose`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`. +- 25 convention plugin ids are registered in `build-logic/convention/build.gradle.kts`. Read that block rather than guessing. The ones module builds apply most often: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.library.compose`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`. diff --git a/.github/instructions/ci-workflows.instructions.md b/.github/instructions/ci-workflows.instructions.md index 6153d1b67f..10c7751d59 100644 --- a/.github/instructions/ci-workflows.instructions.md +++ b/.github/instructions/ci-workflows.instructions.md @@ -9,11 +9,14 @@ excludeAgent: "code-review" - CI uses `.github/ci-gradle.properties` — don't assume local `gradle.properties` values. - CI passes `-Pci=true` to enable full processor usage via `maxParallelForks`. - Use `fetch-depth: 0` only where needed (spotless ratcheting, version code). Use `fetch-depth: 1` otherwise. -- Desktop build matrix: `macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`. -- Lightweight jobs (status gates, labelers, triage, run-cancellers, changelog/release +- Runner labels are named by tier here; the workflows carry the versions. +- Desktop build matrix: `macos-latest`, `windows-latest`, and Ubuntu x64 and arm64; `build-desktop` + in `reusable-check.yml` lists the labels. +- Lightweight jobs off the required-check path (labelers, run-cancellers, changelog/release cleanup): use `ubuntu-slim`. It is container-backed and starts in seconds, but it is - single-CPU, unprivileged, x64-only, and its 15-minute job cap is a hard platform limit — so it + single-CPU, unprivileged, x64-only, and its 15-minute job cap is a hard platform limit. It fits API/script work (`gh`, `jq`, `git`, stdlib `python3`, `github-script`) and nothing that needs `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. -- Lightweight jobs that break any of those constraints: use `ubuntu-24.04-arm` runners. -- Gradle-heavy jobs: use `ubuntu-24.04` runners. +- Lightweight jobs that break any of those constraints, and the required `Check Workflow Status` + gates, which must not queue on slim's separate pool: use the pinned Ubuntu LTS arm label. +- Gradle-heavy jobs: use the pinned Ubuntu LTS x64 label that `reusable-check.yml` uses. diff --git a/.github/renovate.json b/.github/renovate.json index 76dc24c383..bc9a6b3bb3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,9 +14,17 @@ "labels": [ "dependencies" ], - "git-submodules": { - "enabled": true - }, + "ignorePaths": [ + "**/node_modules/**", + "**/bower_components/**", + "**/vendor/**", + "**/examples/**", + "**/__tests__/**", + "**/test/**", + "**/tests/**", + "**/__fixtures__/**", + ".specify/**" + ], "bundler": { "enabled": true }, @@ -51,12 +59,13 @@ "automerge": true }, { - "description": "Meshtastic Protobufs changelog link", + "description": "Protobufs bumps: link the upstream compare in the body (the snapshot version carries the commit as -g; a release maps to its tag). protobufs-bump.yml adds a comment with the merged upstream PRs, the .proto delta and the settings strings that change; the scheduled-updates run the catalog change triggers on main regenerates values/schema_strings.xml from whatever pin main has.", "matchPackageNames": [ - "https://github.com/meshtastic/protobufs.git" + "org.meshtastic:protobufs" ], - "changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}", - "automerge": true + "prBodyNotes": [ + "Upstream: https://github.com/meshtastic/protobufs/compare/{{#if (containsString currentVersion 'SNAPSHOT')}}{{{replace '^.*-g([0-9a-f]+)-SNAPSHOT$' '$1' currentVersion}}}{{else}}v{{{currentVersion}}}{{/if}}...{{#if (containsString newVersion 'SNAPSHOT')}}{{{replace '^.*-g([0-9a-f]+)-SNAPSHOT$' '$1' newVersion}}}{{else}}v{{{newVersion}}}{{/if}} - the `Protobufs Bump` workflow comments with the merged PRs, the `.proto` delta and the settings strings that change; `values/schema_strings.xml` is regenerated by the scheduled-updates run this merge triggers." + ] }, { "description": "Protobufs: only accept the dot-form snapshot scheme (X.Y.Z.N-g-SNAPSHOT) or plain releases. Legacy hyphen-SHA snapshots (e.g. 2.7.26-678281c-SNAPSHOT) tokenize their digit-leading SHA as a huge int that outranks the dot-form commit-count in Renovate's maven comparator, so without this constraint Renovate keeps proposing a downgrade to an old snapshot (see PR #6229). The snapshot repo has no delete API; legacy versions auto-prune after 90 days.", @@ -85,9 +94,6 @@ "/^org\\.jetbrains\\.kotlin/", "/^org\\.jetbrains\\.kotlinx/", "/^org\\.jetbrains\\.compose/", - "/^com\\.google\\.dagger/", - "/^androidx\\.hilt/", - "/^com\\.google\\.protobuf/", "/^androidx\\.lifecycle/", "/^androidx\\.navigation/", "/^androidx\\.datastore/", @@ -116,6 +122,26 @@ "/^io\\.insert-koin\\.compiler\\.plugin/" ], "automerge": false + }, + { + "description": "One weekly PR for everything the github-actions manager finds, since each .github-only PR runs the whole PR pipeline. Majors still get their own PR. Ruby stays out so the setup-ruby inputs move with .ruby-version. Vulnerability fixes are not held: Renovate forces an empty schedule and no group on them.", + "matchManagers": [ + "github-actions" + ], + "matchDepNames": [ + "!ruby" + ], + "groupName": "github-actions", + "schedule": [ + "before 6am on monday" + ] + }, + { + "description": "Ruby waits a week: setup-ruby learns a new Ruby some days after its release, and a bump that reaches main first breaks every job that installs Ruby. The depName covers .ruby-version and the setup-ruby ruby-version inputs alike.", + "matchDepNames": [ + "ruby" + ], + "minimumReleaseAge": "7 days" } ], "customManagers": [ @@ -131,6 +157,32 @@ "depNameTemplate": "gradle", "datasourceTemplate": "gradle-version", "versioningTemplate": "gradle" + }, + { + "customType": "regex", + "description": "ktlint's version is a bare [versions] entry that only Spotless reads, so the gradle manager cannot tie it to an artifact.", + "managerFilePatterns": [ + "/^gradle/libs\\.versions\\.toml$/" + ], + "matchStrings": [ + "\\nktlint\\s*=\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "com.pinterest.ktlint:ktlint-cli", + "datasourceTemplate": "maven", + "versioningTemplate": "maven" + }, + { + "customType": "regex", + "description": "ktfmt's version is a bare [versions] entry that only Spotless reads, so the gradle manager cannot tie it to an artifact.", + "managerFilePatterns": [ + "/^gradle/libs\\.versions\\.toml$/" + ], + "matchStrings": [ + "\\nktfmt\\s*=\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "com.facebook:ktfmt", + "datasourceTemplate": "maven", + "versioningTemplate": "maven" } ] } diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml index 341f4f8f30..304391d5f0 100644 --- a/.github/workflows/create-or-promote-release.yml +++ b/.github/workflows/create-or-promote-release.yml @@ -28,8 +28,6 @@ on: permissions: contents: write - pull-requests: write - statuses: write id-token: write attestations: write @@ -41,13 +39,14 @@ concurrency: jobs: determine-tags: - runs-on: ubuntu-26.04-arm + runs-on: ubuntu-slim timeout-minutes: 10 outputs: tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }} - release_name: ${{ steps.calculate_tags.outputs.release_name }} final_tag: ${{ steps.calculate_tags.outputs.final_tag }} from_channel: ${{ steps.calculate_tags.outputs.from_channel }} + version_name: ${{ steps.version.outputs.version_name }} + version_code: ${{ steps.version.outputs.version_code }} steps: # Internal releases are exempt: Play internal testing skips full review, # so only promotions (closed/open/production) can clobber an in-flight @@ -66,9 +65,14 @@ jobs: - name: Calculate tags id: calculate_tags + env: + BASE_VERSION: ${{ inputs.base_version }} + CHANNEL: ${{ inputs.channel }} run: | - BASE_VERSION="${{ inputs.base_version }}" - CHANNEL="${{ inputs.channel }}" + if [[ ! "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::base_version '$BASE_VERSION' is not X.Y.Z (e.g., 2.8.3)." + exit 1 + fi if [[ "$CHANNEL" == "internal" ]]; then # This is a new build, create a new internal tag @@ -84,7 +88,6 @@ jobs: echo "Calculated new tag: $NEW_TAG" { echo "tag_to_process=$NEW_TAG" - echo "release_name=$NEW_TAG" echo "final_tag=$NEW_TAG" } >> "$GITHUB_OUTPUT" else @@ -121,17 +124,49 @@ jobs: NEW_TAG="v${BASE_VERSION}" fi - echo "New release name will be: $NEW_TAG" echo "Final tag will be: $NEW_TAG" { echo "from_channel=${FROM_CHANNEL}" echo "tag_to_process=${LATEST_TAG_TO_PROMOTE}" - echo "release_name=${NEW_TAG}" echo "final_tag=${NEW_TAG}" } >> "$GITHUB_OUTPUT" fi shell: bash + # Name from the tag, code from the commit count plus VERSION_CODE_OFFSET, both at the + # commit being released: for a promotion the promoted tag's, which main has moved past. + - name: Calculate version + id: version + env: + BASE_VERSION: ${{ inputs.base_version }} + CHANNEL: ${{ inputs.channel }} + TAG: ${{ steps.calculate_tags.outputs.tag_to_process }} + run: | + if [[ "$CHANNEL" == "internal" ]]; then REF=HEAD; else REF="$TAG"; fi + git show "${REF}:config.properties" > "$RUNNER_TEMP/config.properties" + + # The release highlights and the Play what's-new are looked up by VERSION_NAME_BASE. + CONFIG_BASE=$(sed -n 's/^VERSION_NAME_BASE=//p' "$RUNNER_TEMP/config.properties") + if [[ "$CONFIG_BASE" != "$BASE_VERSION" ]]; then + echo "::error::base_version is $BASE_VERSION but config.properties at $REF has VERSION_NAME_BASE=${CONFIG_BASE:-}." + exit 1 + fi + + VERSION_NAME=$(echo "$TAG" | sed 's/-.*//' | sed 's/v//') + VERSION_CODE_OFFSET=$(grep '^VERSION_CODE_OFFSET=' "$RUNNER_TEMP/config.properties" | cut -d'=' -f2 || true) + if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then + echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'" + exit 1 + fi + VERSION_CODE=$(( $(git rev-list --count "$REF") + VERSION_CODE_OFFSET )) + + echo "Version: $VERSION_NAME ($VERSION_CODE) from $REF" + { + echo "version_name=$VERSION_NAME" + echo "version_code=$VERSION_CODE" + } >> "$GITHUB_OUTPUT" + shell: bash + - name: Create and Push Release Tag if: ${{ !inputs.dry_run && inputs.channel == 'internal' }} env: @@ -148,10 +183,8 @@ jobs: uses: ./.github/workflows/release.yml with: tag_name: ${{ needs.determine-tags.outputs.final_tag }} - channel: ${{ inputs.channel }} - base_version: ${{ inputs.base_version }} - build_desktop: true - build_flatpak_src: true + version_name: ${{ needs.determine-tags.outputs.version_name }} + version_code: ${{ needs.determine-tags.outputs.version_code }} secrets: inherit call-promote-workflow: @@ -164,38 +197,65 @@ jobs: # so call-release-workflow doesn't carry it. permissions: contents: write - pull-requests: write - statuses: write - id-token: write - attestations: write actions: write uses: ./.github/workflows/promote.yml with: tag_name: ${{ needs.determine-tags.outputs.tag_to_process }} - release_name: ${{ needs.determine-tags.outputs.release_name }} final_tag: ${{ needs.determine-tags.outputs.final_tag }} channel: ${{ inputs.channel }} base_version: ${{ inputs.base_version }} from_channel: ${{ needs.determine-tags.outputs.from_channel }} + version_code: ${{ needs.determine-tags.outputs.version_code }} secrets: inherit + # A production promotion stamps CHANGELOG.md itself; every other cut refreshes [Unreleased]. + update-changelog: + needs: [call-release-workflow, call-promote-workflow] + if: >- + ${{ !cancelled() && !inputs.dry_run && inputs.channel != 'production' + && (needs.call-release-workflow.result == 'success' || needs.call-promote-workflow.result == 'success') }} + runs-on: ubuntu-slim + timeout-minutes: 5 + permissions: + actions: write + steps: + - name: Dispatch Update Changelog + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh workflow run update-changelog.yml --ref main + cleanup-on-failure: needs: [determine-tags, call-release-workflow] if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }} - runs-on: ubuntu-26.04-arm + runs-on: ubuntu-slim timeout-minutes: 10 + permissions: + contents: write + actions: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - name: Delete Failed or Cancelled Tag env: FINAL_TAG: ${{ needs.determine-tags.outputs.final_tag }} + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} run: | - if [ -n "$FINAL_TAG" ]; then - echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..." - git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted." - else + if [ -z "$FINAL_TAG" ]; then echo "No tag was created to delete." + exit 0 fi + # Play keeps an uploaded versionCode, so once publish-play succeeded the tag stays with it. + PLAY=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \ + --jq '[.jobs[] | select(.name | endswith("publish-play")) | .conclusion][0] // ""') || { + echo "::warning::Could not read this run's jobs, so $FINAL_TAG stays. Delete it by hand if Play has no build from it." + exit 0 + } + if [ "$PLAY" = "success" ]; then + echo "::warning::Play already has the build from $FINAL_TAG, so the tag stays. Re-run the failed jobs to finish the release." + exit 0 + fi + echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..." + git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted." diff --git a/.github/workflows/dependency-graph-submit.yml b/.github/workflows/dependency-graph-submit.yml deleted file mode 100644 index 62972ddffb..0000000000 --- a/.github/workflows/dependency-graph-submit.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Submit Dependency Graph - -# PR runs can only generate-and-upload (fork tokens lack contents: write). This submits what -# they saved, from the base repo's trusted context. Never checks out PR code — the snapshot -# artifact is the only input. -on: - workflow_run: - workflows: ['Pull Request CI'] - types: [completed] - -permissions: - actions: read - contents: write - -# head_branch alone would collide across forks that share a branch name (e.g. two "patch-1"s). -concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true - -jobs: - submit-dependency-graph: - # failed runs may have partial graphs; skip - if: github.repository == 'meshtastic/Meshtastic-Android' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-26.04-arm - timeout-minutes: 10 - steps: - # skipped android-check (docs-only/bot PRs) uploads nothing — don't fail red on that - - name: Check the run saved a dependency graph - id: probe - env: - GH_TOKEN: ${{ github.token }} - run: | - count=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts?per_page=100" \ - --jq '[.artifacts[] | select(.name | startswith("dependency-graph"))] | length' | paste -sd+ | bc) - echo "count=$count" >> "$GITHUB_OUTPUT" - - - name: Download and submit dependency graph - if: steps.probe.outputs.count != '0' - uses: gradle/actions/dependency-submission@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - with: - dependency-graph: download-and-submit diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 8d2525c1f3..f96883a293 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,29 +1,23 @@ name: Deploy Documentation -# Publishes the main-branch docs snapshot to /main/ (and refreshed Dokka to -# /api/) on the persistent gh-pages branch. The site root (latest release) -# and /vX.Y.Z/ folders are owned by docs-release.yml and are left untouched. +# Publishes the main-branch docs to the persistent gh-pages branch: the site +# snapshot at /main/ on pushes that touch a site input, and the Dokka reference +# at /api/ on the daily schedule. A manual run rebuilds both. The site root and +# /vX.Y.Z/ folders are owned by docs-release.yml and are left untouched. on: push: branches: [main] paths: - # Dokka sources (KDoc in source files) - - 'androidApp/src/**' - - 'core/**/src/**' - - 'feature/**/src/**' - - 'desktopApp/src/**' - # Docs site sources - 'docs/**' - - 'feature/docs/**' - # Build infrastructure. Module scripts are included because they can add or - # drop exported `api` dependencies and reshape source sets, changing the - # generated reference without any edit under src/. - - 'build-logic/**' - - '**/build.gradle.kts' - - 'settings.gradle.kts' - - '.github/workflows/docs-deploy.yml' + - 'build-logic/convention/**/Docs*.kt' + - 'specs/20260507-161858-app-docs-markdown/contracts/keyword-index-schema.json' - 'scripts/docs/**' + - '.ruby-version' + - '.github/workflows/docs-deploy.yml' + + schedule: + - cron: '37 5 * * *' workflow_dispatch: @@ -42,92 +36,68 @@ jobs: if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-26.04 timeout-minutes: 45 + env: + BUILD_SITE: ${{ github.event_name != 'schedule' }} + BUILD_API: ${{ github.event_name != 'push' }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: true - fetch-depth: 0 - name: Gradle Setup uses: ./.github/actions/gradle-setup with: gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + # Dokka runs commonizeNativeDistribution, which reads ~/.konan. + cache_konan: 'true' + cache_robolectric: 'false' - name: Setup Ruby - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 + if: env.BUILD_SITE == 'true' + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + # With ruby-version unset this reads the root .ruby-version; the bundle is docs/Gemfile. + env: + BUNDLE_GEMFILE: docs/Gemfile with: - ruby-version: '4.0.7' bundler-cache: true - working-directory: docs - - # Dokka is the slowest part of this workflow (~14 min) but KDoc changes far - # less often than the prose docs. Rebuild /api/ only when something that can - # actually change the generated reference was touched; otherwise the existing - # /api/ on gh-pages is left in place (the publisher overlays per channel). - # Manual runs always rebuild everything, so the diff is only needed on push. - - name: Detect Dokka-relevant changes - if: github.event_name != 'workflow_dispatch' - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4 - id: filter - with: - token: '' - filters: | - # Positive patterns only. paths-filter evaluates each pattern as an - # independent predicate and ORs them together, so a '!excluded/**' - # entry would match every path outside that dir and make the filter - # always true. The modules in DOKKA_EXCLUDED_MODULES that live under - # these prefixes (:core:konsist) therefore still trigger a rebuild; - # they change rarely enough that the odd wasted run is fine. - dokka: - - 'androidApp/src/**' - - 'core/**/src/**' - - 'feature/**/src/**' - - 'desktopApp/src/**' - # Dokka config, module list and the plugin classpath. '**/build.gradle.kts' - # covers the root script as well as every module's, since a module script - # can change exported `api` deps or source sets with no src/ edit. - - 'build-logic/**' - - '**/build.gradle.kts' - - 'settings.gradle.kts' - - 'gradle/libs.versions.toml' - name: Generate Docs Site (main channel) + if: env.BUILD_SITE == 'true' run: ./gradlew generateDocsBundle validateDocsBundle publishDocsSite -Pdocs.channel=main -Pci=true - # Dokka (Gradle) and Jekyll (Ruby) are independent — Dokka's output is only - # copied in afterwards — so run them concurrently to overlap the two slowest - # steps (~14 min Dokka vs ~5 min Jekyll) instead of summing them. - - name: Build Dokka + Jekyll concurrently - env: - BUILD_DOKKA: ${{ steps.filter.outputs.dokka == 'true' || github.event_name == 'workflow_dispatch' }} + # Jekyll reads only the generated site and Dokka only the sources, so a run + # that builds both overlaps them. + - name: Build Jekyll and Dokka run: | set -euo pipefail - BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \ - --source build/_site/main \ - --destination build/jekyll_site \ - --baseurl /${{ github.event.repository.name }}/main & - jekyll_pid=$! - if [ "$BUILD_DOKKA" = "true" ]; then - ./gradlew dokkaGeneratePublicationHtml - else - echo "No Dokka-relevant changes — skipping API reference rebuild." + jekyll_pid="" + if [ "$BUILD_SITE" = "true" ]; then + BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \ + --source build/_site/main \ + --destination build/jekyll_site \ + --baseurl "/${GITHUB_REPOSITORY#*/}/main" & + jekyll_pid=$! + fi + if [ "$BUILD_API" = "true" ]; then + ./gradlew :dokkaGeneratePublicationHtml + fi + if [ -n "$jekyll_pid" ]; then + wait "$jekyll_pid" fi - wait "$jekyll_pid" - name: Stage channels id: stage - env: - BUILD_DOKKA: ${{ steps.filter.outputs.dokka == 'true' || github.event_name == 'workflow_dispatch' }} run: | set -euo pipefail mkdir -p build/pages_staging - cp -r build/jekyll_site build/pages_staging/main - channels="main" - if [ "$BUILD_DOKKA" = "true" ]; then + channels="" + if [ "$BUILD_SITE" = "true" ]; then + cp -r build/jekyll_site build/pages_staging/main + channels="main" + fi + if [ "$BUILD_API" = "true" ]; then cp -r build/dokka/html build/pages_staging/api - channels="$channels api" + channels="${channels:+$channels }api" fi echo "channels=$channels" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/docs-link-check.yml b/.github/workflows/docs-link-check.yml new file mode 100644 index 0000000000..668137c21d --- /dev/null +++ b/.github/workflows/docs-link-check.yml @@ -0,0 +1,86 @@ +name: Docs Link Check + +# Checks the web links in docs/en every week and keeps one open issue listing +# the broken ones. docs-quality.yml checks the internal links on every docs PR. + +on: + schedule: + - cron: '23 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +# One run at a time, so two runs can never both find no issue and open two. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + check: + if: github.repository == 'meshtastic/Meshtastic-Android' + runs-on: ubuntu-slim + timeout-minutes: 15 + permissions: + contents: read + issues: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # http(s) only: Jekyll's extensionless page links are not files lychee can + # resolve. Discord rate-limits invite lookups, and a 429 is not a broken link. + - name: Check links + id: lychee + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + fail: false + args: >- + --no-progress + --scheme https + --scheme http + --max-retries 3 + --accept '100..=103,200..=299,429' + --exclude '^https?://(www\.)?discord\.(gg|com)/' + 'docs/en/**/*.md' + + # lychee exits 2 when links are broken; any other non-zero code is the + # check itself failing, which fails the run instead of filing an issue. + - name: Open or update the broken-links issue + if: steps.lychee.outputs.exit_code != '0' + env: + GH_TOKEN: ${{ github.token }} + EXIT_CODE: ${{ steps.lychee.outputs.exit_code }} + TITLE: Broken links in docs/en + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + if [ "$EXIT_CODE" != "2" ]; then + echo "lychee exited $EXIT_CODE; see the Check links step." >&2 + exit 1 + fi + { printf 'Found by the weekly link check: %s\n\n' "$RUN_URL"; cat lychee/out.md; } > "$RUNNER_TEMP/body.md" + number=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --label documentation \ + --search "\"$TITLE\" in:title" --json number,title \ + --jq '[.[] | select(.title == env.TITLE)][0].number // empty') + if [ -n "$number" ]; then + gh issue edit "$number" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/body.md" + else + gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label documentation \ + --body-file "$RUNNER_TEMP/body.md" + fi + + - name: Close the broken-links issue + if: steps.lychee.outputs.exit_code == '0' + env: + GH_TOKEN: ${{ github.token }} + TITLE: Broken links in docs/en + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + number=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --label documentation \ + --search "\"$TITLE\" in:title" --json number,title \ + --jq '[.[] | select(.title == env.TITLE)][0].number // empty') + if [ -n "$number" ]; then + gh issue close "$number" --repo "$GITHUB_REPOSITORY" --comment "Every link passed in $RUN_URL." + fi diff --git a/.github/workflows/docs-quality.yml b/.github/workflows/docs-quality.yml index bac2bf06e2..f47fd189e0 100644 --- a/.github/workflows/docs-quality.yml +++ b/.github/workflows/docs-quality.yml @@ -13,7 +13,9 @@ on: branches: [main] paths: - "docs/en/**" + - "docs/**/*.md" - "feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt" + - "scripts/docs/sync-locale-front-matter.py" - "scripts/check-doc-aliases.js" - "scripts/check-doc-coverage.js" - "scripts/check-doc-freshness.js" @@ -103,6 +105,11 @@ jobs: # frontmatter and never registered is a search term no consumer ever sees. run: node scripts/check-doc-aliases.js . + - name: Check locale front matter + # layout and nav_order in every docs// page must match docs/en, and no locale + # page may have a parent or grand_parent, which would list it in an English section. + run: python3 scripts/docs/sync-locale-front-matter.py --check + - name: Check doc freshness # Advisory, as it always was: a page being old is a prompt to look, not a defect. continue-on-error: true diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index 7363a1ee3b..3e979e30a2 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -2,23 +2,24 @@ name: Docs Release # Publishes release docs to the persistent gh-pages branch. # -# Production tags (vX.Y.Z) own the site root and get a permanent /vX.Y.Z/ copy, -# plus a refreshed Dokka reference at /api/. +# promote.yml dispatches this on the tag after every open, closed and production +# promotion, because a tag created with GITHUB_TOKEN starts no workflow. The tag +# trigger covers a tag pushed by hand, and a manual run against a tag ref +# (re)publishes that version without cutting a new tag. +# +# Production tags (vX.Y.Z) own the site root and get a /vX.Y.Z/ copy, plus a +# refreshed Dokka reference at /api/. # # Open- and closed-testing tags (vX.Y.Z-open.N / vX.Y.Z-closed.N) publish a # per-tag snapshot at /vX.Y.Z-open.N/ only. They deliberately do NOT touch the # root or /api/: the root belongs to production releases, and /api/ is an -# unversioned channel already kept current by docs-deploy.yml on every push to -# main — rebuilding Dokka (~14 min) for each of the many prerelease tags in a -# cycle would cost far more than it refreshes. +# unversioned channel that docs-deploy.yml rebuilds from main every day. # # These per-tag prerelease directories accumulate during a version cycle and are -# reaped by post-release-cleanup.yml once the production vX.Y.Z tag ships. +# reaped by post-release-cleanup.yml, which a production publish dispatches. That +# cleanup also removes every production copy older than the newest one below /vX.Y.Z/. # # The /main/ snapshot is owned by docs-deploy.yml and is left untouched here. -# -# workflow_dispatch exists for backfill: run it against a tag ref to (re)publish -# that version without cutting a new tag. on: push: @@ -43,12 +44,14 @@ jobs: if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-26.04 timeout-minutes: 45 + # actions: write is only for the cleanup dispatch; job permissions replace the + # workflow block, so contents: write is restated. + permissions: + contents: write + actions: write steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: true - fetch-depth: 0 # Resolves the tag into: the docs version label (which becomes the # published directory name) and whether this is a production release. @@ -84,13 +87,17 @@ jobs: with: gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + # Dokka runs commonizeNativeDistribution, which reads ~/.konan. + cache_konan: 'true' + cache_robolectric: 'false' - name: Setup Ruby - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + # With ruby-version unset this reads the root .ruby-version; the bundle is docs/Gemfile. + env: + BUNDLE_GEMFILE: docs/Gemfile with: - ruby-version: '4.0.7' bundler-cache: true - working-directory: docs # Versioned docs (/vX.Y.Z/ or /vX.Y.Z-open.N/) — built for every channel. - name: Build Versioned Docs @@ -99,34 +106,39 @@ jobs: # Root site (/) — production releases only. - name: Build Root Docs Site if: steps.version.outputs.is_production == 'true' - run: ./gradlew generateDocsBundle publishDocsSite -Pdocs.channel=root -Pci=true + run: ./gradlew publishDocsSite -Pdocs.channel=root -Pci=true - # Dokka API reference (/api/) — production releases only. - - name: Build Dokka HTML documentation - if: steps.version.outputs.is_production == 'true' - run: ./gradlew dokkaGeneratePublicationHtml - - - name: Compile Jekyll Sites + # The versioned source leaves build/_site/ first so the root site does not + # nest it. The Jekyll builds read only the generated sites and Dokka (the + # /api/ reference, production only) only the sources, so all three overlap. + - name: Build Jekyll Sites and Dokka env: DOCS_VERSION: ${{ steps.version.outputs.docs_version }} IS_PRODUCTION: ${{ steps.version.outputs.is_production }} run: | set -euo pipefail - # Versioned site - BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \ - --source "build/_site/v${DOCS_VERSION}" \ - --destination build/jekyll_release \ - --baseurl "/${{ github.event.repository.name }}/v${DOCS_VERSION}" - - # Move the versioned source out of the root source tree so the root - # build below doesn't try to nest it. + site_name="${GITHUB_REPOSITORY#*/}" mv "build/_site/v${DOCS_VERSION}" build/v_temp + BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \ + --source build/v_temp \ + --destination build/jekyll_release \ + --baseurl "/${site_name}/v${DOCS_VERSION}" & + release_pid=$! + + root_pid="" if [ "$IS_PRODUCTION" = "true" ]; then BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \ --source build/_site \ --destination build/jekyll_root \ - --baseurl "/${{ github.event.repository.name }}" + --baseurl "/${site_name}" & + root_pid=$! + ./gradlew :dokkaGeneratePublicationHtml + fi + + wait "$release_pid" + if [ -n "$root_pid" ]; then + wait "$root_pid" fi - name: Stage channels @@ -148,3 +160,12 @@ jobs: - name: Publish to gh-pages run: scripts/docs/publish-to-gh-pages.sh build/pages_staging ${{ steps.stage.outputs.channels }} + + # The cleanup refuses to reap until /vX.Y.Z/ is on gh-pages, which the push above + # just made true. A dispatch is exempt from GITHUB_TOKEN's event suppression. + - name: Dispatch post-release cleanup + if: steps.version.outputs.is_production == 'true' + env: + GH_TOKEN: ${{ github.token }} + BASE_VERSION: ${{ steps.version.outputs.docs_version }} + run: gh workflow run post-release-cleanup.yml --ref main -f "base_version=$BASE_VERSION" -f confirm_deletion=true diff --git a/.github/workflows/main-check.yml b/.github/workflows/main-check.yml index 6a84f466ab..912147a3c7 100644 --- a/.github/workflows/main-check.yml +++ b/.github/workflows/main-check.yml @@ -6,13 +6,16 @@ on: paths-ignore: - '**/*.md' - 'docs/**' + - 'fastlane/**' + - 'obtainium/**' permissions: contents: read +# Not cancelled: one run plus one pending per ref, so every run that starts finishes. concurrency: group: main-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: # Every commit on main arrives via the merge queue, which already ran lint, diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index 8fbab45724..29e607b473 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -8,28 +8,34 @@ permissions: contents: read # Note: github.ref is unique per merge-group entry (gh-readonly-queue/main/pr-N-), -# so this group never dedupes across re-queues of the same PR — the cancel-superseded -# job below handles that explicitly. +# so this group never dedupes across re-queues of the same PR. check-changes cancels +# those runs itself. concurrency: group: build-mq-${{ github.ref }} cancel-in-progress: true jobs: - # When a PR is re-queued (an entry ahead of it failed or was removed), GitHub creates a - # new merge group but does NOT cancel the workflow runs of the destroyed one. Those stale - # runs sit queued/running and starve the runner pool. Cancel any older merge-queue run - # for the same PR — only the newest merge group per PR is ever valid. - # No checkout, no toolchain — just gh api + jq. Runs on the same label as check-changes: - # ubuntu-slim is a separate, smaller pool, and a gate job waiting on it holds the whole queue. - cancel-superseded: - name: Cancel Superseded Queue Runs + # Docs-only queue entries (changelog updates, markdown fixes, store listings, Obtainium + # configs) cannot affect the build; skip the heavy pipeline for them. Anything outside + # docs/, fastlane/, obtainium/ and *.md runs full CI. Mirrors the paths-ignore list in + # main-check.yml. + # No checkout, no toolchain: gh api only. + check-changes: + name: Check Changes if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-26.04-arm timeout-minutes: 5 permissions: actions: write + contents: read + outputs: + android: ${{ steps.filter.outputs.android }} steps: + # A re-queued PR gets a new merge group, but GitHub leaves the destroyed group's runs + # queued or running, starving the runner pool. Only the newest group per PR is valid. + # Best effort: a failed cancel must not fail the required check. - name: Cancel older merge-queue runs for the same PR + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | @@ -50,28 +56,18 @@ jobs: gh api -X POST "repos/${{ github.repository }}/actions/runs/${run_id}/force-cancel" || true done - # Docs-only queue entries (changelog updates, markdown fixes) cannot affect the build; - # skip the heavy pipeline for them. Anything outside docs/ and *.md runs full CI. - # Mirrors the paths-ignore list in main-check.yml. - check-changes: - name: Check Changes - if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-26.04-arm - timeout-minutes: 5 - outputs: - android: ${{ steps.filter.outputs.android }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - name: Diff merge group against its base id: filter + env: + GH_TOKEN: ${{ github.token }} run: | - git fetch --depth=1 origin "${{ github.event.merge_group.base_sha }}" - changed=$(git diff --name-only "${{ github.event.merge_group.base_sha }}" "${{ github.event.merge_group.head_sha }}") - echo "Changed files:" + changed=$(gh api "repos/${{ github.repository }}/compare/${{ github.event.merge_group.base_sha }}...${{ github.event.merge_group.head_sha }}" \ + --jq '.files[].filename') + count=$(grep -c . <<<"$changed" || true) + echo "Changed files ($count):" echo "$changed" - if echo "$changed" | grep -qvE '^docs/|\.md$|^$'; then + # The compare API lists at most 300 files, so a list that long may be truncated. + if [ "$count" -ge 300 ] || echo "$changed" | grep -qvE '^docs/|^fastlane/|^obtainium/|\.md$|^$'; then echo "android=true" >> "$GITHUB_OUTPUT" else echo "android=false" >> "$GITHUB_OUTPUT" @@ -99,14 +95,16 @@ jobs: upload_artifacts: false secrets: inherit - # Pure gate job: no checkout, no toolchain, just reads `needs` results. It is the required - # check, so it runs on the label the rest of the workflow already gets slots on, not on - # ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. + # Gate job: no checkout, no toolchain, reads `needs` results and posts license/cla. It is the + # required check, so it runs on a hosted Ubuntu label that shares the org's pool with the build + # jobs, not on ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. check-workflow-status: name: Check Workflow Status runs-on: ubuntu-26.04-arm timeout-minutes: 5 - permissions: {} + permissions: + pull-requests: read + statuses: write needs: - check-changes - android-check @@ -118,8 +116,35 @@ jobs: echo "::error::Change detection failed" exit 1 fi - if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled") ]]; then + if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then echo "::error::Android Check failed" exit 1 fi echo "All jobs passed successfully" + + # license/cla is required on the group commit too, but cla-assistant.io only checks PRs and + # its placeholder for the group never arrives when its webhook drops, stalling the queue. + - name: Post license/cla for the merge group + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + REF_NAME: ${{ github.ref_name }} + HEAD_SHA: ${{ github.event.merge_group.head_sha }} + run: | + if [[ ! "$REF_NAME" =~ /pr-([0-9]+)- ]]; then + echo "::error::Could not parse PR from ref '$REF_NAME'" + exit 1 + fi + pr="${BASH_REMATCH[1]}" + pr_sha=$(gh api "repos/$REPO/pulls/$pr" --jq '.head.sha') + cla=$(gh api "repos/$REPO/commits/$pr_sha/status" \ + --jq '[.statuses[] | select(.context == "license/cla")][0].state // "missing"') + if [[ "$cla" != "success" ]]; then + echo "::error::license/cla is $cla on PR #$pr head $pr_sha" + exit 1 + fi + gh api -X POST "repos/$REPO/statuses/$HEAD_SHA" \ + -f state=success -f context=license/cla \ + -f "target_url=https://cla-assistant.io/$REPO" \ + -f "description=CLA signed on PR #$pr. CLA checks only happen on pull requests." \ + --jq '"posted \(.context) \(.state) on \(.url)"' diff --git a/.github/workflows/msstore-publish.yml b/.github/workflows/msstore-publish.yml index fe3892e4dc..7999324fc9 100644 --- a/.github/workflows/msstore-publish.yml +++ b/.github/workflows/msstore-publish.yml @@ -46,6 +46,13 @@ jobs: # than silently skipping. HAS_MSSTORE_CREDS: ${{ secrets.MSSTORE_PRODUCT_ID != '' && 'true' || 'false' }} steps: + # A skipped submission must not read as a published one. + - name: Report an unconfigured Store publish + if: env.HAS_MSSTORE_CREDS != 'true' + run: | + echo "::warning::MSSTORE_PRODUCT_ID is not set; nothing was submitted to the Microsoft Store." + echo "- Microsoft Store: skipped, MSSTORE_* secrets not set" >> "$GITHUB_STEP_SUMMARY" + # The Store requires a versioned, immutable installer URL — per-tag # GitHub release asset URLs are exactly that. Fails loudly if the # release carries no MSI (e.g. the Windows build leg failed) rather diff --git a/.github/workflows/play-listing.yml b/.github/workflows/play-listing.yml index 052e7c745c..4c163b1ba3 100644 --- a/.github/workflows/play-listing.yml +++ b/.github/workflows/play-listing.yml @@ -1,10 +1,11 @@ name: Play Store Listing # Uploads fastlane/metadata/android - every locale's title, descriptions, -# feature graphic, icon and screenshots - to the Google Play listing. Manual -# only: a listing commit is a Play submission, and a submission sent for review -# cancels and restarts any release review in flight. Nothing here touches a -# build or a track. +# feature graphic, icon and screenshots - to the Google Play listing from the +# committed tree. Every promotion does this from the tag (promote.yml), so this +# is the hand path for a listing change between releases. A listing commit is a +# Play submission, and a submission sent for review cancels and restarts any +# release review in flight. Nothing here touches a build or a track. # # The default is a dry run. Play validates the whole edit (unknown locales, # overlength text, bad image dimensions) and discards it; tick `publish` to @@ -43,13 +44,14 @@ jobs: run: python3 scripts/check-store-metadata.py - name: Set up Ruby - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 with: - ruby-version: '4.0.7' bundler-cache: true - name: Decode Play Store credentials - run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json + env: + GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} + run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json - name: Upload listing env: diff --git a/.github/workflows/play-rollout.yml b/.github/workflows/play-rollout.yml new file mode 100644 index 0000000000..51a67bb394 --- /dev/null +++ b/.github/workflows/play-rollout.yml @@ -0,0 +1,171 @@ +name: Play Rollout + +# Moves the staged rollout already on a Play track: the one inProgress release a promotion +# leaves there (production at 10%, beta at 50%). rollout widens it to `fraction`, complete +# ships it to every user, halt stops it where it is. supply acts only on inProgress releases, +# so a halted release is resumed or completed in the Play Console. +on: + workflow_dispatch: + inputs: + track: + description: 'Play track holding the staged release' + required: true + type: choice + options: + - production + - beta + action: + description: 'rollout widens it to fraction, complete ships it to everyone, halt stops it' + required: true + type: choice + options: + - rollout + - complete + - halt + fraction: + description: 'rollout only: the new user fraction, above the current one and below 1 (e.g., 0.5)' + required: false + type: string + no_review_in_flight: + description: 'I checked Publishing overview > Submission activity and no submission is In review. A committed rollout change CANCELS and RESTARTS any review in flight.' + required: false + type: boolean + default: false + +permissions: + contents: read + +# Not the promotion's group: a group keeps one pending run, so this would cancel a pending promotion. +concurrency: + group: play-rollout + cancel-in-progress: false + +jobs: + rollout: + if: github.repository == 'meshtastic/Meshtastic-Android' + runs-on: ubuntu-26.04-arm + timeout-minutes: 15 + env: + TRACK: ${{ inputs.track }} + ACTION: ${{ inputs.action }} + steps: + - name: Require review-in-flight confirmation + if: ${{ !inputs.no_review_in_flight }} + run: | + echo "::error::Rollout change blocked: confirm no Play review is in flight. Check Play Console > Publishing overview > Submission activity; if a submission shows 'In review', wait. If clear, re-dispatch with 'no_review_in_flight' checked." + exit 1 + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + bundler-cache: true + + - name: Decode Play Store credentials + env: + GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} + run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json + + # Exactly one inProgress release, or nothing is changed: that release's version code + # pins supply's selection, and its fraction is what a halt keeps. + - name: Find the staged release + id: staged + env: + FRACTION: ${{ inputs.fraction }} + run: | + bundle exec fastlane play_track_releases track:"$TRACK" out:"$RUNNER_TEMP/before.json" + STAGED=$(jq -c '[.[] | select(.status == "inProgress")]' "$RUNNER_TEMP/before.json") + if [[ $(jq length <<< "$STAGED") -ne 1 ]]; then + echo "::error::Track '$TRACK' needs exactly one inProgress release; it holds: $(jq -c . "$RUNNER_TEMP/before.json")" + exit 1 + fi + VERSION_CODE=$(jq -r '.[0].version_codes | max' <<< "$STAGED") + CURRENT=$(jq -r '.[0].user_fraction // empty' <<< "$STAGED") + if [[ ! "$VERSION_CODE" =~ ^[0-9]+$ || ! "$CURRENT" =~ ^0?\.[0-9]+$ ]]; then + echo "::error::The inProgress release on '$TRACK' has no usable version code or fraction: $STAGED" + exit 1 + fi + + STATUS="" + case "$ACTION" in + rollout) + if [[ ! "$FRACTION" =~ ^0?\.[0-9]+$ ]] || ! awk -v c="$CURRENT" -v n="$FRACTION" 'BEGIN { exit !(n > c && n < 1) }'; then + echo "::error::fraction '$FRACTION' must be above the current $CURRENT and below 1; use complete to ship to everyone." + exit 1 + fi + ROLLOUT="$FRACTION" + ;; + complete) ROLLOUT=1 ;; + halt) + ROLLOUT="$CURRENT" + STATUS=halted + ;; + esac + echo "versionCode $VERSION_CODE on '$TRACK' at $CURRENT: $ACTION to $ROLLOUT${STATUS:+ ($STATUS)}" + { + echo "version_code=$VERSION_CODE" + echo "current=$CURRENT" + echo "rollout=$ROLLOUT" + echo "status=$STATUS" + } >> "$GITHUB_OUTPUT" + + # Every upload is skipped, so the edit changes only this release's status and fraction. + - name: Change the rollout + env: + VERSION_CODE: ${{ steps.staged.outputs.version_code }} + ROLLOUT: ${{ steps.staged.outputs.rollout }} + STATUS: ${{ steps.staged.outputs.status }} + run: | + bundle exec fastlane supply \ + --track "$TRACK" \ + --version_code "$VERSION_CODE" \ + --rollout "$ROLLOUT" \ + ${STATUS:+--release_status "$STATUS"} \ + --skip_upload_apk \ + --skip_upload_aab \ + --skip_upload_metadata \ + --skip_upload_changelogs \ + --skip_upload_images \ + --skip_upload_screenshots + + # supply exits 0 even when its edit changed nothing. The script's verify counts only + # completed and inProgress releases, so a halt is confirmed from a fresh read instead. + - name: Verify the release on the track + env: + VERSION_CODE: ${{ steps.staged.outputs.version_code }} + CURRENT: ${{ steps.staged.outputs.current }} + ROLLOUT: ${{ steps.staged.outputs.rollout }} + run: | + PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2) + if [[ "$ACTION" != "halt" ]]; then + bash scripts/play-track-preflight.sh \ + fastlane/play-store-credentials.json "$PKG" "$TRACK" "$VERSION_CODE" verify + fi + case "$ACTION" in + rollout) WANT=inProgress ;; + complete) WANT=completed ;; + halt) WANT=halted ;; + esac + bundle exec fastlane play_track_releases track:"$TRACK" out:"$RUNNER_TEMP/after.json" + AFTER=$(jq -c --argjson vc "$VERSION_CODE" '[.[] | select(.version_codes | index($vc))] | first // empty' "$RUNNER_TEMP/after.json") + if [[ "$(jq -r '.status' <<< "${AFTER:-null}")" != "$WANT" ]]; then + echo "::error::versionCode $VERSION_CODE on '$TRACK' is not $WANT after the change: ${AFTER:-absent}" + exit 1 + fi + if [[ "$ACTION" == "rollout" ]] && ! awk -v a="$(jq -r '.user_fraction' <<< "$AFTER")" -v w="$ROLLOUT" 'BEGIN { exit !(a == w) }'; then + echo "::error::versionCode $VERSION_CODE on '$TRACK' is not at $ROLLOUT after the change: $AFTER" + exit 1 + fi + { + echo "## Play rollout: ${TRACK}" + echo + echo "versionCode ${VERSION_CODE}: ${ACTION}, from ${CURRENT} to $(jq -r '.user_fraction // "all users"' <<< "$AFTER") (${WANT})." + echo + echo "A change Play could not send for review on its own waits under Publishing overview in the Play Console." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Clean up credentials + if: always() + run: rm -f fastlane/play-store-credentials.json diff --git a/.github/workflows/post-release-cleanup.yml b/.github/workflows/post-release-cleanup.yml index 78dc4a5c76..d5773b0223 100644 --- a/.github/workflows/post-release-cleanup.yml +++ b/.github/workflows/post-release-cleanup.yml @@ -1,5 +1,7 @@ name: Post-Release Cleanup +# docs-release.yml dispatches this with confirm_deletion=true once a production tag's +# /vX.Y.Z/ is on gh-pages. A manual dispatch is the retry path and defaults to a dry run. on: workflow_dispatch: inputs: @@ -37,6 +39,18 @@ jobs: env: BASE_VERSION: ${{ github.event.inputs.base_version }} CONFIRM_DELETION: ${{ github.event.inputs.confirm_deletion }} + # Keeps the vX.Y.Z-* names on stdin whose X.Y.Z is at or below BASE_VERSION. Anything + # above it belongs to a later cycle, whose draft release promote.yml still needs. + AT_OR_BELOW: | + { + v = $0; sub(/^v/, "", v); sub(/-.*/, "", v) + split(v, a, "."); split(base, b, ".") + for (i = 1; i <= 3; i++) { + if (a[i] + 0 < b[i] + 0) { print; next } + if (a[i] + 0 > b[i] + 0) next + } + print + } steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -65,12 +79,12 @@ jobs: # v2.7.14-internal.N draft built while working toward 2.8.0). Scoping this # sweep to "v${BASE_VERSION}-*" misses exactly those, leaving stale drafts # behind forever. Once base_version has shipped as a stable release, every - # numbered internal/open/closed pre-release — regardless of which version - # it was tagged under — is superseded, so match on the pre-release SHAPE - # instead of a version prefix. + # numbered internal/open/closed pre-release at or below it is superseded, + # so match on the pre-release SHAPE capped by AT_OR_BELOW instead of a + # version prefix. TAG_PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-(internal|open|closed)\.[0-9]+$' - echo "Searching for pre-releases matching pattern '$TAG_PATTERN'." - RELEASES_TO_DELETE=$(gh release list --json tagName,isPrerelease,isDraft --limit 1000 | jq -r --arg pattern "$TAG_PATTERN" '.[] | select((.isPrerelease == true or .isDraft == true) and .tagName != null and (.tagName | test($pattern))) | .tagName') + echo "Searching for pre-releases matching pattern '$TAG_PATTERN' at or below $BASE_VERSION." + RELEASES_TO_DELETE=$(gh release list --json tagName,isPrerelease,isDraft --limit 1000 | jq -r --arg pattern "$TAG_PATTERN" '.[] | select((.isPrerelease == true or .isDraft == true) and .tagName != null and (.tagName | test($pattern))) | .tagName' | awk -v base="$BASE_VERSION" "$AT_OR_BELOW") if [ -z "$RELEASES_TO_DELETE" ]; then echo "No stale internal/open/closed pre-releases found." @@ -88,8 +102,9 @@ jobs: # The docs site keeps a per-tag snapshot for every open/closed testing tag # in a version cycle (see docs-release.yml). Once vX.Y.Z ships, /vX.Y.Z/ - # supersedes them all, so reap them to stop gh-pages growing without bound. - - name: Cleanup pre-release docs snapshots on gh-pages + # supersedes them all, so reap them. Production copies older than the + # release before vX.Y.Z go too, to keep gh-pages under the Pages size limit. + - name: Cleanup superseded docs on gh-pages run: | set -euo pipefail DRY_RUN=true @@ -121,29 +136,51 @@ jobs: # dev cycle, including before the version-bump commit lands, so some may # still be named after the PRIOR release (e.g. v2.7.14-open.3 built while # working toward 2.8.0). Once /v${BASE_VERSION}/ is confirmed published - # above, every numbered open/closed snapshot is superseded regardless of - # which version it was tagged under — so match the snapshot-dir SHAPE - # instead of a version prefix. + # above, every numbered open/closed snapshot at or below it is superseded, + # so match the snapshot-dir SHAPE capped by AT_OR_BELOW instead of a + # version prefix. mapfile -t stale < <( find "$work" -maxdepth 1 -mindepth 1 -type d \ -regextype posix-extended \ - -regex ".*/v[0-9]+\.[0-9]+\.[0-9]+-(open|closed)\.[0-9]+" -printf '%f\n' | sort + -regex ".*/v[0-9]+\.[0-9]+\.[0-9]+-(open|closed)\.[0-9]+" -printf '%f\n' \ + | awk -v base="$BASE_VERSION" "$AT_OR_BELOW" | sort ) - if [ ${#stale[@]} -eq 0 ]; then - echo "No pre-release docs snapshots found." + # Production copies strictly below base_version, oldest first. The newest of them + # stays beside base_version; nothing above base_version is listed, so a backfill + # dispatch for an old version never deletes newer docs. + mapfile -t older < <( + find "$work" -maxdepth 1 -mindepth 1 -type d \ + -regextype posix-extended \ + -regex ".*/v[0-9]+\.[0-9]+\.[0-9]+" -printf '%f\n' \ + | awk -v base="$BASE_VERSION" "$AT_OR_BELOW" \ + | grep -vxF "v${BASE_VERSION}" | sort -V || true + ) + culled=() + if [ ${#older[@]} -gt 1 ]; then + culled=("${older[@]:0:${#older[@]}-1}") + fi + + if [ ${#stale[@]} -eq 0 ] && [ ${#culled[@]} -eq 0 ]; then + echo "No superseded docs found." exit 0 fi - printf 'Pre-release docs snapshots to reap:\n' - printf ' %s\n' "${stale[@]}" + if [ ${#stale[@]} -gt 0 ]; then + printf 'Pre-release docs snapshots to reap:\n' + printf ' %s\n' "${stale[@]}" + fi + if [ ${#culled[@]} -gt 0 ]; then + printf 'Production docs older than %s to remove:\n' "${older[-1]}" + printf ' %s\n' "${culled[@]}" + fi if [ "$DRY_RUN" = true ]; then echo "DRY RUN: the directories above would be removed from gh-pages." exit 0 fi - for d in "${stale[@]}"; do + for d in "${stale[@]}" "${culled[@]}"; do rm -rf "${work:?}/$d" done @@ -159,9 +196,9 @@ jobs: fi git -c user.name='github-actions[bot]' \ -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ - commit -q -m "docs: reap pre-release snapshots for ${BASE_VERSION}" + commit -q -m "docs: reap superseded docs for ${BASE_VERSION}" git push --quiet origin HEAD:gh-pages - echo "Removed ${#stale[@]} pre-release docs snapshot(s) from gh-pages." + echo "Removed ${#stale[@]} pre-release and ${#culled[@]} production docs directories from gh-pages." - name: Cleanup dangling pre-release tags env: @@ -169,8 +206,8 @@ jobs: run: | set -euo pipefail # Same rationale as the release-cleanup step above: match the - # internal/open/closed pre-release tag SHAPE across all versions, not just - # tags prefixed with this dispatch's base_version. + # internal/open/closed pre-release tag SHAPE at or below base_version, + # not just tags prefixed with it. TAG_PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-(internal|open|closed)\.[0-9]+$' echo "Searching for any remaining remote pre-release tags matching pattern '$TAG_PATTERN'." @@ -181,7 +218,7 @@ jobs: REMOTE_TAGS=$(git ls-remote --tags origin "refs/tags/v*" | awk '{print $2}' | sed 's|refs/tags/||') # Some tags may have been deleted already by the previous 'release delete' step. - TAGS_TO_DELETE=$(grep -E "$TAG_PATTERN" <<<"$REMOTE_TAGS" || true) + TAGS_TO_DELETE=$(grep -E "$TAG_PATTERN" <<<"$REMOTE_TAGS" | awk -v base="$BASE_VERSION" "$AT_OR_BELOW" || true) if [ -z "$TAGS_TO_DELETE" ]; then echo "No dangling pre-release tags found." diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index cca261a3c3..d4fa30247c 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -11,18 +11,10 @@ on: description: 'The tag that triggered the release' required: true type: string - release_name: - description: 'The desired name for the GitHub release' - required: true - type: string final_tag: description: 'The final tag for the release' required: true type: string - commit_sha: - description: 'The commit SHA to tag' - required: false - type: string channel: description: 'The channel to promote to' required: true @@ -31,29 +23,26 @@ on: description: 'The channel to promote from' required: true type: string + version_code: + description: 'The version code of the build being promoted' + required: true + type: string secrets: - GSERVICES: - required: true - KEYSTORE: - required: true - KEYSTORE_FILENAME: - required: true - KEYSTORE_PROPERTIES: - required: true - DATADOG_APPLICATION_ID: - required: true - DATADOG_CLIENT_TOKEN: - required: true - GOOGLE_MAPS_API_KEY: - required: true GOOGLE_PLAY_JSON_KEY: required: true - GRADLE_ENCRYPTION_KEY: - required: true DISCORD_WEBHOOK_ANDROID: required: false HOMEBREW_TAP_TOKEN: required: false + CROWDIN_GITHUB_TOKEN: + required: false + FLATHUB_TOKEN: + required: false + # Read only for presence, so the checklist says whether each store workflow will skip. + WINGET_TOKEN: + required: false + MSSTORE_PRODUCT_ID: + required: false # Never cancel a promotion mid-flight: being killed between the Play edit # commit and the GitHub release/tag update leaves the two disagreeing. The @@ -65,59 +54,15 @@ concurrency: permissions: contents: write - pull-requests: write - statuses: write - id-token: write - attestations: write jobs: - prepare-build-info: - runs-on: ubuntu-26.04-arm - timeout-minutes: 10 - outputs: - APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} - APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.commit_sha || inputs.tag_name }} - fetch-depth: 0 - submodules: 'recursive' - - - name: Prep APP_VERSION_NAME - id: prep_version - env: - INPUT_TAG_NAME: ${{ inputs.tag_name }} - run: | - VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//') - echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT" - echo "Parsed Version: $VERSION_NAME" - - - name: Extract VERSION_CODE_OFFSET from config.properties - id: get_version_code_offset - run: | - OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2) - echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT" - - - name: Calculate Version Code from Git Commit Count - id: calculate_version_code - env: - VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }} - run: | - COMMIT_COUNT=$(git rev-list --count HEAD) - if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then - echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'" - exit 1 - fi - VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET)) - echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT" - shell: bash - promote-release: runs-on: ubuntu-26.04-arm timeout-minutes: 30 - needs: [ prepare-build-info ] + outputs: + already_on_track: ${{ steps.preflight.outputs.already_on_track }} + screenshots: ${{ steps.screenshots.outputs.found }} + listing: ${{ steps.listing.outcome }} env: FROM_TRACK: ${{ inputs.from_channel == 'closed' && 'NewAlpha' || (inputs.from_channel == 'open' && 'beta' || 'internal') }} TO_TRACK: ${{ inputs.channel == 'closed' && 'NewAlpha' || (inputs.channel == 'open' && 'beta' || 'production') }} @@ -126,16 +71,17 @@ jobs: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.commit_sha || inputs.tag_name }} + ref: ${{ inputs.tag_name }} - name: Set up Ruby - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 with: - ruby-version: '4.0.7' bundler-cache: true - name: Decode Play Store credentials - run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json + env: + GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} + run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json # A re-dispatched promotion whose versionCode is already live on the # target track must no-op: every redundant `supply` commit creates a new @@ -165,7 +111,7 @@ jobs: - name: Preflight — is this versionCode already on the target track? id: preflight env: - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + VERSION_CODE: ${{ inputs.version_code }} run: | PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2) bash .workflow-ref/scripts/play-track-preflight.sh \ @@ -174,10 +120,14 @@ jobs: # --version_code pins release selection. Without it supply picks the # source release by status (default "completed"), so a staged inProgress # rollout on the source track silently promotes the OLD release instead. + # Changelogs are not skipped: with --track_promote_to, supply writes each + # locale's changelogs/default.txt (rendered from metainfo.xml by + # scripts/sync-play-changelog.py, translated by Crowdin) onto the promoted + # release in the same edit, so the Play "What's new" is never empty. - name: Promote to next channel if: ${{ steps.preflight.outputs.already_on_track != 'true' }} env: - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + VERSION_CODE: ${{ inputs.version_code }} run: | bundle exec fastlane supply \ --track "$FROM_TRACK" \ @@ -186,7 +136,6 @@ jobs: --rollout "$USER_FRACTION" \ --changes_not_sent_for_review true \ --skip_upload_metadata \ - --skip_upload_changelogs \ --skip_upload_images \ --skip_upload_screenshots @@ -195,52 +144,135 @@ jobs: - name: Verify versionCode landed on the target track if: ${{ steps.preflight.outputs.already_on_track != 'true' }} env: - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + VERSION_CODE: ${{ inputs.version_code }} run: | PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2) bash .workflow-ref/scripts/play-track-preflight.sh \ fastlane/play-store-credentials.json "$PKG" "$TO_TRACK" "$VERSION_CODE" verify + # The listing is the tag's text for every locale plus the google-flavor screenshots + # the internal cut captured and attached to the release. Without the attachment + # the committed set goes up instead, and the checklist says so. + # An absent asset (a tag cut before the render job existed) falls back to + # the committed set. A present asset that fails to download is an error, + # and the listing step stays out rather than publish the wrong images. + - name: Fetch the rendered store screenshots + id: screenshots + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.tag_name }} + run: | + ASSET=$(gh release view "$TAG" --json assets --jq '.assets[].name | select(startswith("store-listing-screenshots-google-"))' | head -1) + if [[ -z "$ASSET" ]]; then + echo "::warning::No rendered screenshots attached to $TAG; the listing keeps the committed set." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + gh release download "$TAG" --pattern "$ASSET" --dir "$RUNNER_TEMP/shots" + unzip -qo "$RUNNER_TEMP/shots/$ASSET" -d fastlane/metadata/android/en-US/images + echo "found=true" >> "$GITHUB_OUTPUT" + + # A dry run on the testing tracks, so a bad locale or image dimension fails + # here and not on production, where the edit is committed. Held as "changes + # not sent for review" either way; Play does not restart a release review + # for it. Soft-fail: the track promotion above already happened. + - name: Publish the Play listing + id: listing + if: ${{ steps.screenshots.outcome == 'success' }} + continue-on-error: true + env: + VALIDATE_ONLY: ${{ inputs.channel != 'production' && 'true' || 'false' }} + run: | + python3 .workflow-ref/scripts/check-store-metadata.py + bundle exec fastlane play_listing validate_only:"$VALIDATE_ONLY" + - name: Clean up credentials if: always() run: rm -f fastlane/play-store-credentials.json update-github-release: - runs-on: ubuntu-26.04-arm + runs-on: ubuntu-slim timeout-minutes: 10 - needs: [ prepare-build-info, promote-release ] + needs: [ promote-release ] # actions: write is scoped here — only this job's publish-workflow # dispatch needs it, and the other jobs must not get it. Job-level # permissions replace the workflow-level block, so the full set this - # job uses is listed. + # job uses is listed. Its PRs are opened with CROWDIN_GITHUB_TOKEN. permissions: contents: write - pull-requests: write - statuses: write actions: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.commit_sha || inputs.tag_name }} + ref: ${{ inputs.tag_name }} fetch-depth: 0 - submodules: 'recursive' - - name: Push Git Tag on Success - if: ${{ inputs.commit_sha != '' }} - run: | - git tag ${{ inputs.final_tag }} ${{ inputs.commit_sha }} - git push origin ${{ inputs.final_tag }} + # Same reasoning as promote-release: scripts and the bot-pr action come from the + # caller's commit, not the tag, so a tag cut before either landed still gets it. + - name: Checkout release scripts from caller commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + path: .workflow-ref + sparse-checkout: | + scripts + .github/actions/bot-pr + # A rerun finds the release already moved to the final tag, so that is looked up first. - name: Update GitHub Release with gh CLI + id: release env: GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.tag_name }} + FINAL_TAG: ${{ inputs.final_tag }} + VERSION_CODE: ${{ inputs.version_code }} + PRERELEASE: ${{ inputs.channel != 'production' }} run: | - gh release edit ${{ inputs.tag_name }} \ - --tag ${{ inputs.final_tag }} \ - --title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \ + CURRENT=$(gh release view "$FINAL_TAG" --json tagName --jq .tagName 2>/dev/null || echo "$TAG") + gh release edit "$CURRENT" \ + --tag "$FINAL_TAG" \ + --title "$FINAL_TAG ($VERSION_CODE)" \ --draft=false \ - --prerelease=${{ inputs.channel != 'production' }} + --prerelease="$PRERELEASE" + + # The draft's notes were generated at the internal cut and cover only the + # PRs since the previous published pre-release. A production release is + # what users read, so regenerate its notes over the whole range since the + # previous production tag, highlights first. The raw notes are kept for + # the CHANGELOG.md stamp below. Soft-fail: the drafted notes stay in place + # when this step cannot produce better ones. + - name: Rewrite release notes for the full range + id: notes + if: ${{ inputs.channel == 'production' }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.final_tag }} + VERSION: ${{ inputs.base_version }} + REPO: ${{ github.repository }} + run: | + PREV_PROD=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^v${VERSION}$" | head -1) + if [[ -z "$PREV_PROD" ]]; then + echo "::warning::No previous production tag found; leaving the drafted release notes in place." + exit 1 + fi + echo "Previous production tag: $PREV_PROD" + + gh api "repos/${REPO}/releases/generate-notes" \ + -f tag_name="$TAG" \ + -f target_commitish="$(git rev-parse HEAD)" \ + -f previous_tag_name="$PREV_PROD" \ + --jq '.body' > "$RUNNER_TEMP/notes-raw.md" + + cp "$RUNNER_TEMP/notes-raw.md" "$RUNNER_TEMP/release-notes.md" + python3 .workflow-ref/scripts/release-highlights.py \ + desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml \ + config.properties \ + "$RUNNER_TEMP/release-notes.md" + gh release edit "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" # The edit above is made with GITHUB_TOKEN, and GITHUB_TOKEN-caused # events never start workflow runs — winget-publish.yml's `released` @@ -248,6 +280,7 @@ jobs: # suppression, so dispatch it explicitly. Soft-fail: a winget hiccup # must not block the changelog stamp or Discord notification. - name: Dispatch winget publish + id: winget if: ${{ inputs.channel == 'production' }} continue-on-error: true env: @@ -257,6 +290,7 @@ jobs: # Same GITHUB_TOKEN-suppression story as the winget dispatch above. - name: Dispatch Microsoft Store publish + id: msstore if: ${{ inputs.channel == 'production' }} continue-on-error: true env: @@ -264,35 +298,65 @@ jobs: TAG: ${{ inputs.final_tag }} run: gh workflow run msstore-publish.yml --ref main -f "tag=$TAG" - - name: Stamp CHANGELOG.md for release + # Same suppression again: version-bump.yml opens the next patch line on main. + - name: Dispatch version bump + id: bump if: ${{ inputs.channel == 'production' }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.final_tag }} + run: gh workflow run version-bump.yml --ref main -f "tag=$TAG" + + # docs-release.yml's tag trigger never fires either: the release edit + # above creates the tag with GITHUB_TOKEN. Run it on the tag ref, which + # publishes /vX.Y.Z/ (plus the root and /api/ for production) or the + # /vX.Y.Z-open.N/ snapshot. The tag appears a moment after the undraft. + - name: Dispatch docs release + id: docs + if: ${{ inputs.channel != 'internal' }} + continue-on-error: true env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.final_tag }} - VERSION: ${{ inputs.base_version }} REPO: ${{ github.repository }} + run: | + for _ in 1 2 3 4 5 6; do + gh api "repos/${REPO}/git/ref/tags/${TAG}" --silent && break + sleep 10 + done + gh workflow run docs-release.yml --ref "$TAG" + + # The Obtainium table in README.md follows the channel releases; this refreshes it + # now rather than at the next scheduled run. + - name: Dispatch scheduled updates + id: sched + if: ${{ inputs.channel != 'internal' }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run scheduled-updates.yml --ref main + + # bot-pr resets the checked-out branch to its origin copy before branching the PR, + # so the edits below are made on a local main, never on a branch origin lacks. + - name: Stamp CHANGELOG.md for release + id: stamp + if: ${{ inputs.channel == 'production' }} + env: + VERSION: ${{ inputs.base_version }} run: | DATE=$(date -u +%Y-%m-%d) - # Checkout CHANGELOG.md from main (we're on a tag checkout) git fetch origin main - git checkout -b "changelog/v${VERSION}" origin/main + git checkout -B main origin/main - # Find the previous production tag for the full-range notes - PREV_PROD=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^v${VERSION}$" | head -1) - - # Generate a single flat changelog for the entire release using GitHub API - FLAT_NOTES="" - if [ -n "$PREV_PROD" ]; then - FLAT_NOTES=$(gh api "repos/${REPO}/releases/generate-notes" \ - -f tag_name="$TAG" \ - -f target_commitish="$(git rev-parse "$TAG")" \ - -f previous_tag_name="$PREV_PROD" \ - --jq '.body' 2>/dev/null || true) - - # Strip boilerplate - FLAT_NOTES=$(echo "$FLAT_NOTES" \ - | sed '/^/d' \ + # The full-range notes from the step above, minus the boilerplate. + # The file is the record whether or not the release edit went + # through; the checklist reports that edit. No file means generation + # failed, and the section says so instead of stamping empty. + FLAT_NOTES="*Release notes were not generated; run the Update Changelog workflow.*" + if [ -s "$RUNNER_TEMP/notes-raw.md" ]; then + FLAT_NOTES=$(sed '/^/d' "$RUNNER_TEMP/notes-raw.md" \ | sed '/^## What'\''s Changed$/d' \ | sed '/^\*\*Full Changelog\*\*/d' \ | sed '/./,$!d' \ @@ -331,33 +395,178 @@ jobs: f.write(new_content) " "$VERSION" "$DATE" "$FLAT_NOTES" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add CHANGELOG.md - git diff --cached --quiet || { - BRANCH="automation/changelog-v${VERSION}" - git checkout -B "$BRANCH" - git commit -m "docs: release CHANGELOG.md for v${VERSION}" - git push origin "$BRANCH" --force + - name: Open the CHANGELOG.md PR + id: stamp_pr + if: ${{ inputs.channel == 'production' }} + uses: ./.workflow-ref/.github/actions/bot-pr + with: + token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + branch: automation/changelog-v${{ inputs.base_version }} + title: 'docs: release CHANGELOG.md for v${{ inputs.base_version }}' + body: 'Automated changelog stamp for production release v${{ inputs.base_version }}.' + add-paths: CHANGELOG.md + labels: | + automation + skip-changelog - gh pr create \ - --title "docs: release CHANGELOG.md for v${VERSION}" \ - --body "Automated changelog stamp for production release v${VERSION}." \ - --head "$BRANCH" \ - --base main \ - --label "automation" \ - --label "skip-changelog" + # F-Droid and IzzyOnDroid read fastlane/ straight from git, so the committed set + # follows what production shipped: the fdroid-flavor captures come back from the + # release into a bot PR that merges itself, with the desktop set beside them. The + # Flathub metainfo reads the release assets directly and needs nothing here. + # Nothing is opened when the files already match. + - name: Stage the store screenshots from the release + id: shots + if: ${{ inputs.channel == 'production' }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.final_tag }} + run: | + ASSET=$(gh release view "$TAG" --json assets --jq '.assets[].name | select(startswith("store-listing-screenshots-fdroid-"))' | head -1) + if [[ -z "$ASSET" ]]; then + echo "::warning::No rendered screenshots attached to $TAG; the committed set stays." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + gh release download "$TAG" --pattern "$ASSET" --pattern 'meshtastic-desktop-*.png' --dir "$RUNNER_TEMP/shots" + git fetch origin main + git checkout -B main origin/main + unzip -qo "$RUNNER_TEMP/shots/$ASSET" -d fastlane/metadata/android/en-US/images + cp "$RUNNER_TEMP"/shots/meshtastic-desktop-*.png desktopApp/packaging/linux/screenshots/ + echo "found=true" >> "$GITHUB_OUTPUT" - # Post required commit status so the PR isn't blocked - COMMIT_SHA=$(git rev-parse HEAD) - gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \ - -f state="success" \ - -f context="Check Workflow Status" \ - -f description="Skipped — changelog-only PR" + - name: Refresh the committed store screenshots + id: shots_pr + if: ${{ steps.shots.outputs.found == 'true' }} + continue-on-error: true + uses: ./.workflow-ref/.github/actions/bot-pr + with: + token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + branch: automation/store-screenshots-v${{ inputs.base_version }} + title: 'chore(store): refresh the committed screenshots from v${{ inputs.base_version }}' + body: 'The screenshots rendered for the v${{ inputs.base_version }} release, so the fastlane tree F-Droid reads matches what shipped.' + add-paths: | + fastlane/metadata/android/en-US/images + desktopApp/packaging/linux/screenshots + labels: | + automation + skip-changelog + + # One place to read what this promotion did, what it dispatched, and what + # is still done by hand. The hand-done list stays out of the Discord post, + # which is an announcement. + - name: Release checklist + if: ${{ always() }} + env: + CHANNEL: ${{ inputs.channel }} + TAG: ${{ inputs.final_tag }} + REPO: ${{ github.repository }} + ALREADY_ON_TRACK: ${{ needs.promote-release.outputs.already_on_track }} + SCREENSHOTS: ${{ needs.promote-release.outputs.screenshots }} + LISTING: ${{ needs.promote-release.outputs.listing }} + SHOTS: ${{ steps.shots.outcome }} + SHOTS_FOUND: ${{ steps.shots.outputs.found }} + SHOTS_PR: ${{ steps.shots_pr.outputs.url }} + SHOTS_PR_OUTCOME: ${{ steps.shots_pr.outcome }} + RELEASE: ${{ steps.release.outcome }} + NOTES: ${{ steps.notes.outcome }} + DOCS: ${{ steps.docs.outcome }} + SCHED: ${{ steps.sched.outcome }} + WINGET: ${{ steps.winget.outcome }} + MSSTORE: ${{ steps.msstore.outcome }} + BUMP: ${{ steps.bump.outcome }} + STAMP: ${{ steps.stamp.outcome }} + STAMP_PR: ${{ steps.stamp_pr.outputs.url }} + STAMP_PR_OUTCOME: ${{ steps.stamp_pr.outcome }} + HAS_FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN != '' }} + HAS_WINGET_TOKEN: ${{ secrets.WINGET_TOKEN != '' }} + HAS_MSSTORE_PRODUCT_ID: ${{ secrets.MSSTORE_PRODUCT_ID != '' }} + HAS_HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN != '' }} + run: | + row() { printf '| %s | %s |\n' "$1" "$2"; } + # A store workflow skips inside itself when its secret is unset, so a successful + # dispatch alone does not mean anything was submitted. + store_row() { + if [[ "$2" != "success" ]]; then + row "$1" "$2" + elif [[ "$3" != "true" ]]; then + row "$1" "will skip: $4 unset" + else + row "$1" "dispatched" + fi } + { + echo "## Release checklist: ${TAG} (${CHANNEL})" + echo + echo "| Step | Result |" + echo "|---|---|" + if [[ "$ALREADY_ON_TRACK" == "true" ]]; then + row "Play promotion" "no-op, versionCode already on the track" + else + row "Play promotion" "done, changelogs uploaded" + fi + row "GitHub release" "$RELEASE - https://github.com/${REPO}/releases/tag/${TAG}" + if [[ "$SCREENSHOTS" == "true" ]]; then + row "Store screenshots" "rendered set from the release" + else + row "Store screenshots" "none attached, committed set used" + fi + if [[ "$CHANNEL" == "production" ]]; then + row "Play listing published" "$LISTING" + else + row "Play listing validated (dry run)" "$LISTING" + fi + row "Docs Release dispatched" "$DOCS" + row "Scheduled Updates dispatched (Obtainium table)" "$SCHED" + if [[ "$CHANNEL" == "production" ]]; then + if [[ -n "$SHOTS_PR" ]]; then + row "Committed screenshots refresh PR" "$SHOTS_PR" + elif [[ "$SHOTS" != "success" ]]; then + row "Committed screenshots refresh PR" "$SHOTS" + elif [[ "$SHOTS_FOUND" != "true" ]]; then + row "Committed screenshots refresh PR" "none needed (no rendered set attached)" + elif [[ "$SHOTS_PR_OUTCOME" == "success" ]]; then + row "Committed screenshots refresh PR" "none needed (committed set matches)" + else + row "Committed screenshots refresh PR" "$SHOTS_PR_OUTCOME" + fi + row "Release notes rewritten (full range)" "$NOTES" + if [[ -n "$STAMP_PR" ]]; then + row "CHANGELOG.md stamp PR" "$STAMP_PR" + elif [[ "$STAMP" == "success" && "$STAMP_PR_OUTCOME" == "success" ]]; then + row "CHANGELOG.md stamp PR" "none needed (already stamped)" + else + row "CHANGELOG.md stamp PR" "stamp $STAMP, PR ${STAMP_PR_OUTCOME:-skipped}" + fi + store_row "winget" "$WINGET" "$HAS_WINGET_TOKEN" WINGET_TOKEN + store_row "Microsoft Store" "$MSSTORE" "$HAS_MSSTORE_PRODUCT_ID" MSSTORE_PRODUCT_ID + row "Version bump dispatched" "$BUMP" + row "Post-Release Cleanup" "dispatched by Docs Release once /${TAG}/ is published" + if [[ "$HAS_HOMEBREW_TAP_TOKEN" == "true" ]]; then + row "Homebrew cask PR" "opened by the update-homebrew-cask job after this one" + else + row "Homebrew cask PR" "will skip: HOMEBREW_TAP_TOKEN unset" + fi + if [[ "$HAS_FLATHUB_TOKEN" == "true" ]]; then + row "Flathub update PR" "opened by the update-flathub job after this one" + else + row "Flathub update PR" "will skip: FLATHUB_TOKEN unset" + fi + echo + echo "Still by hand:" + echo "- Play: the production rollout is staged; widen, complete or halt it with the Play Rollout workflow." + if [[ "$HAS_FLATHUB_TOKEN" == "true" ]]; then + echo "- Flathub: merge the update-flathub PR once its test build passes." + else + echo "- Flathub: bump flathub/org.meshtastic.MeshtasticDesktop (tag, commit, gradle zip and sha256, flatpak-sources.json)." + fi + echo "- Release notes: replace the placeholder the version bump PR wrote for the next line." + fi + } >> "$GITHUB_STEP_SUMMARY" + # Announces once the release is published, whatever happened to the steps after it. - name: Notify Discord - if: ${{ inputs.channel != 'internal' }} + if: ${{ always() && steps.release.outcome == 'success' && inputs.channel != 'internal' }} env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ANDROID }} VERSION: ${{ inputs.final_tag }} @@ -408,14 +617,14 @@ jobs: # promoted to homebrew/cask, replace the tap PR with `brew bump-cask-pr`. update-homebrew-cask: if: ${{ inputs.channel == 'production' }} - runs-on: ubuntu-26.04-arm + runs-on: ubuntu-slim timeout-minutes: 15 needs: [ update-github-release ] steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.commit_sha || inputs.tag_name }} + ref: ${{ inputs.tag_name }} - name: Render cask and open PR against meshtastic/homebrew-tap env: @@ -423,12 +632,14 @@ jobs: TAG: ${{ inputs.final_tag }} run: | if [[ -z "$TAP_TOKEN" ]]; then - echo "HOMEBREW_TAP_TOKEN not set; skipping Homebrew cask update." + echo "::warning::HOMEBREW_TAP_TOKEN not set; skipping Homebrew cask update." + echo "- Homebrew cask: skipped, HOMEBREW_TAP_TOKEN not set" >> "$GITHUB_STEP_SUMMARY" exit 0 fi # Tags cut before the template landed check out without it — skip, don't fail. if [[ ! -f .github/homebrew/meshtastic-desktop.rb ]]; then - echo "Cask template not present at this tag; skipping Homebrew cask update." + echo "::warning::Cask template not present at this tag; skipping Homebrew cask update." + echo "- Homebrew cask: skipped, no template at this tag" >> "$GITHUB_STEP_SUMMARY" exit 0 fi @@ -459,8 +670,88 @@ jobs: fi git commit -m "meshtastic-desktop ${VERSION}" git push -fu origin "$BRANCH" - GH_TOKEN="$TAP_TOKEN" gh pr create --repo meshtastic/homebrew-tap \ + PR_URL=$(GH_TOKEN="$TAP_TOKEN" gh pr create --repo meshtastic/homebrew-tap \ --head "$BRANCH" --base main \ --title "meshtastic-desktop ${VERSION}" \ --body "Automated cask bump for the ${TAG} production release of [Meshtastic-Android](https://github.com/${{ github.repository }}/releases/tag/${TAG})." \ - || echo "PR already exists for $BRANCH; branch updated." + || echo "PR already exists for $BRANCH; branch updated.") + echo "- Homebrew cask: $PR_URL" >> "$GITHUB_STEP_SUMMARY" + + # The same for flathub/org.meshtastic.MeshtasticDesktop: the tag and its commit, the Gradle + # zip that tag's wrapper pins, and the release's flatpak-sources.json. The JBR, runtime and + # patches stay as they are; Flathub's test build on the PR checks them against the tag. + update-flathub: + if: ${{ inputs.channel == 'production' }} + runs-on: ubuntu-slim + timeout-minutes: 15 + needs: [ update-github-release ] + steps: + # From the caller's commit, like the scripts in the jobs above. + - name: Checkout the manifest bump script from caller commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + sparse-checkout: scripts/verify-flatpak + + - name: Update the manifest and open PR against flathub/org.meshtastic.MeshtasticDesktop + env: + FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.final_tag }} + REPO: ${{ github.repository }} + run: | + if [[ -z "$FLATHUB_TOKEN" ]]; then + echo "::notice::FLATHUB_TOKEN not set; skipping the Flathub update." + echo "- Flathub: skipped, FLATHUB_TOKEN not set" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + VERSION=${TAG#v} + + # The tag appears a moment after the undraft. + TYPE="" COMMIT="" + for _ in 1 2 3 4 5 6; do + if OBJ=$(gh api "repos/${REPO}/git/ref/tags/${TAG}" --jq '.object.type + " " + .object.sha' 2>/dev/null); then + read -r TYPE COMMIT <<< "$OBJ" + break + fi + sleep 10 + done + if [[ -z "$COMMIT" ]]; then + echo "::error::Tag $TAG not found." + exit 1 + fi + if [[ "$TYPE" == "tag" ]]; then + COMMIT=$(gh api "repos/${REPO}/git/tags/${COMMIT}" --jq .object.sha) + fi + + gh api "repos/${REPO}/contents/gradle/wrapper/gradle-wrapper.properties?ref=${COMMIT}" \ + -H 'Accept: application/vnd.github.raw' > "$RUNNER_TEMP/gradle-wrapper.properties" + gh release download "$TAG" --repo "$REPO" --pattern flatpak-sources.json --dir "$RUNNER_TEMP" + jq empty "$RUNNER_TEMP/flatpak-sources.json" + + FLATHUB="$RUNNER_TEMP/flathub" + git clone "https://x-access-token:${FLATHUB_TOKEN}@github.com/flathub/org.meshtastic.MeshtasticDesktop.git" "$FLATHUB" + BASE=$(git -C "$FLATHUB" rev-parse --abbrev-ref HEAD) + BRANCH="update-${VERSION}" + git -C "$FLATHUB" checkout -b "$BRANCH" + python3 scripts/verify-flatpak/bump-flathub-manifest.py \ + "$FLATHUB/org.meshtastic.MeshtasticDesktop.yaml" "$TAG" "$COMMIT" "$RUNNER_TEMP/gradle-wrapper.properties" + cp "$RUNNER_TEMP/flatpak-sources.json" "$FLATHUB/flatpak-sources.json" + + cd "$FLATHUB" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add org.meshtastic.MeshtasticDesktop.yaml flatpak-sources.json + if git diff --cached --quiet; then + echo "Flathub manifest already at ${TAG}." + echo "- Flathub: already at ${TAG}" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + git commit -m "Update to ${TAG}" + git push -fu origin "$BRANCH" + PR_URL=$(GH_TOKEN="$FLATHUB_TOKEN" gh pr create --repo flathub/org.meshtastic.MeshtasticDesktop \ + --head "$BRANCH" --base "$BASE" \ + --title "Update to ${TAG}" \ + --body "$(printf 'Update Meshtastic Desktop to version %s\nhttps://github.com/%s/releases/tag/%s\n' "$TAG" "$REPO" "$TAG")" \ + || echo "PR already exists for $BRANCH; branch updated.") + echo "- Flathub: $PR_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/protobufs-bump.yml b/.github/workflows/protobufs-bump.yml new file mode 100644 index 0000000000..d930eab07c --- /dev/null +++ b/.github/workflows/protobufs-bump.yml @@ -0,0 +1,107 @@ +name: Protobufs Bump + +# A PR that moves the org.meshtastic:protobufs pin gets one sticky comment saying what the bump carries: the +# upstream PRs merged in between, the .proto delta, and the settings strings android will show differently once +# values/schema_strings.xml is regenerated. The regeneration itself is not done here: the scheduled-updates run +# this merge triggers on main syncs the file from whatever pin main has and carries it, with the Crowdin upload, in +# its own PR. Nothing is pushed to this branch, so Renovate keeps rebasing and automerging it. + +on: + pull_request: + branches: [main] + paths: + - gradle/libs.versions.toml + +permissions: + contents: read + pull-requests: write + +concurrency: + group: protobufs-bump-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + summarise: + runs-on: ubuntu-26.04 + timeout-minutes: 30 + # A fork PR gets a read-only token, so the comment could not be posted anyway. + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout PR head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + filter: 'blob:none' + + - name: Read the protobufs pin on both sides + id: pin + env: + BASE_REF: ${{ github.base_ref }} + run: | + line='meshtastic-protobufs = ' + old=$(git show "origin/$BASE_REF:gradle/libs.versions.toml" | sed -n "s/^${line}\"\(.*\)\"$/\1/p") + new=$(sed -n "s/^${line}\"\(.*\)\"$/\1/p" gradle/libs.versions.toml) + echo "old=$old" >> "$GITHUB_OUTPUT" + echo "new=$new" >> "$GITHUB_OUTPUT" + if [ "$old" = "$new" ]; then + echo "protobufs pin unchanged ($new); nothing to do" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "protobufs $old -> $new" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Gradle Setup + if: steps.pin.outputs.changed == 'true' + uses: ./.github/actions/gradle-setup + with: + gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + job_summary_pr_comment: 'never' + cache_read_only: 'true' + cache_robolectric: 'false' + + # The sync is run to see what it would change; the result is not committed here. + - name: Preview the schema strings at the new pin + if: steps.pin.outputs.changed == 'true' + run: | + cp core/resources/src/commonMain/composeResources/values/schema_strings.xml /tmp/schema_strings.before.xml + ./gradlew :schema-strings:sync -Pci=true + cp core/resources/src/commonMain/composeResources/values/schema_strings.xml /tmp/schema_strings.after.xml + git checkout -- core/resources/src/commonMain/composeResources/values/schema_strings.xml + + - name: Clone protobufs at both commits + if: steps.pin.outputs.changed == 'true' + run: git clone --filter=blob:none --quiet https://github.com/meshtastic/protobufs.git /tmp/protobufs + + - name: Write the summary + if: steps.pin.outputs.changed == 'true' + env: + OLD_PIN: ${{ steps.pin.outputs.old }} + NEW_PIN: ${{ steps.pin.outputs.new }} + run: | + python3 scripts/protobufs-bump-summary.py "$OLD_PIN" "$NEW_PIN" \ + --protobufs /tmp/protobufs \ + --before /tmp/schema_strings.before.xml \ + --after /tmp/schema_strings.after.xml \ + > /tmp/summary.md + { + echo + echo "_values/schema_strings.xml is regenerated by the scheduled-updates run this merge triggers on main._" + } >> /tmp/summary.md + cat /tmp/summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Post or refresh the sticky comment + if: steps.pin.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + existing=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \ + --jq '.[] | select(.body | startswith("")) | .id' | head -n1) + if [ -n "$existing" ]; then + gh api --method PATCH "repos/$REPO/issues/comments/$existing" -F body=@/tmp/summary.md > /dev/null + else + gh pr comment "$PR" --body-file /tmp/summary.md + fi diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index 4b65e50e49..fffd1472b3 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -5,9 +5,10 @@ on: # Do not execute arbitrary code on this workflow. # See warnings at https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true +# No concurrency group. Renovate labels its PR right after opening it, so a cancelling group left a cancelled +# `labeler` check run on the head commit, and Renovate reads any cancelled check run as a pending branch it never +# automerges. A non-cancelling group still cancels a queued run when a third event arrives. The job is idempotent +# and seconds on ubuntu-slim, so every run can simply finish. jobs: labeler: @@ -117,7 +118,7 @@ jobs: const author = context.payload.pull_request.user.login; const headRef = context.payload.pull_request.head.ref; const skipAuthors = ['renovate[bot]', 'github-actions[bot]', 'dependabot[bot]']; - const skipRefs = ['scheduled-updates', 'l10n_main']; + const skipRefs = ['scheduled-updates']; if (!skipAuthors.includes(author) && !skipRefs.includes(headRef)) { const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'desktop', 'chore', 'ci', 'build', 'testing', 'documentation']; const effectiveLabels = new Set([ diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index aa80b706d4..40017bf115 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -17,7 +17,9 @@ jobs: # (folded into this job rather than run standalone: runner-pool slots, not # compute, are the scarce resource during queue bursts). check-changes: - if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) + # scheduled-baseline changes only the googleRelease baseline profile and READMEs, which + # no PR job builds; the merge queue still runs everything. + if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'scheduled-baseline' ) runs-on: ubuntu-26.04-arm timeout-minutes: 10 outputs: @@ -61,8 +63,10 @@ jobs: desktop: - 'desktopApp/**' - 'scripts/build-appimage.sh' - # Shared build machinery that shapes the packaged output. + # Shared build machinery that shapes the packaged output. config/ holds the + # ProGuard rules desktopApp's release jars read and the license texts it bundles. - 'build-logic/**' + - 'config/**' - 'gradle/wrapper/**' - 'gradle/libs.versions.toml' # Root inputs the packaging tasks actually read: config.properties supplies the @@ -88,7 +92,8 @@ jobs: - 'feature/**' - 'screenshot-tests/**' - 'docs-screenshots/**' - - 'marketing-screenshots/**' + - 'store-screenshots/**' + - 'schema-strings/**' # Shared build infrastructure - 'build-logic/**' - 'config/**' @@ -103,182 +108,20 @@ jobs: - 'settings.gradle.kts' - 'test.gradle.kts' - name: Verify module roots are represented in check-changes filter - run: | - python3 - <<'PY' - import re - from pathlib import Path - - settings = Path('settings.gradle.kts').read_text() - workflow = Path('.github/workflows/pull-request.yml').read_text() - - module_roots = { - module.split(':')[0] - for module in re.findall(r'":([^"]+)"', settings) - } - - allowed_extra_roots = {'baselineprofile'} - expected_roots = module_roots | allowed_extra_roots - - filter_paths = { - path.split('/')[0] - for path in re.findall(r"-\s*'([^']+/\*\*)'", workflow) - } - - # Filter roots that are intentionally not Gradle module roots - # (CI/workflow implementation + shared build infrastructure). - allowed_infra_roots = {'.github', 'build-logic', 'config', 'gradle'} - - missing = sorted(expected_roots - filter_paths) - unexpected = sorted(filter_paths - expected_roots - allowed_infra_roots) - - if missing or unexpected: - print('check-changes filter drift detected:') - if missing: - print(' Missing roots:', ', '.join(missing)) - if unexpected: - print(' Unexpected roots:', ', '.join(unexpected)) - raise SystemExit(1) - - print('check-changes filter is aligned with settings.gradle module roots.') - PY - # Drift guard: ALL_MODULES_FULL in RootConventionPlugin.kt is a hand-maintained - # copy of settings.gradle.kts (subprojects {} iteration is incompatible with - # Isolated Projects). It has drifted before: :feature:discovery, :feature:docs - # and :feature:map-maplibre were never added, so they were silently absent from - # Dokka aggregation, Kover aggregation and kmpSmokeCompile. + run: python3 scripts/check-changes-filter.py - name: Verify the root module list matches settings.gradle.kts - run: | - python3 - <<'PY' - import re - from pathlib import Path - - settings = Path('settings.gradle.kts').read_text() - plugin = Path( - 'build-logic/convention/src/main/kotlin/RootConventionPlugin.kt' - ).read_text() - - include = settings[settings.index('include('):] - modules = set(re.findall(r'"(:[^"]+)"', include[: include.index('\n)')])) - - # ALL_MODULES_FULL appears twice (declaration and use), so bound the slice to - # the declaration's own closing paren rather than searching for the name. - decl = plugin[plugin.index('ALL_MODULES_FULL ='):] - listed = set(re.findall(r'"(:[^"]+)"', decl[: decl.index('\n )')])) - - # Test harnesses and generators, deliberately kept out of root aggregation - # (PR #6412). Excluded here so the guard does not force them back in. - exempt = { - ':baselineprofile', - ':core:konsist', - ':docs-screenshots', - ':marketing-screenshots', - ':screenshot-tests', - } - - missing = sorted(modules - listed - exempt) - extra = sorted(listed - modules) - # The exemption is bidirectional: an exempt module must also stay OUT of the - # list. Without this, adding one back would pass silently and quietly undo - # #6412 by pulling a test harness into Dokka, Kover and kmpSmokeCompile. - readded = sorted(listed & exempt) - - problems = [] - for m in missing: - problems.append( - f'{m} is in settings.gradle.kts but not ALL_MODULES_FULL -- it is absent ' - 'from Dokka/Kover aggregation and kmpSmokeCompile' - ) - for m in extra: - problems.append(f'{m} is in ALL_MODULES_FULL but no longer in settings.gradle.kts') - for m in readded: - problems.append( - f'{m} is exempt from root aggregation (#6412) but present in ' - 'ALL_MODULES_FULL -- remove it, or drop it from the exempt set here' - ) - - if problems: - print('Root module list drift detected:') - for p in problems: - print(' -', p) - raise SystemExit(1) - - print(f'{len(listed)} modules verified against settings.gradle.kts ' - f'({len(exempt)} exempt).') - PY - # Drift guard: the shard task lists in reusable-check.yml are - # hand-maintained and have silently dropped modules before (discovery, - # docs, wifi-provision, car, datastore, konsist had tests that never ran - # in CI). Every module in settings.gradle.kts must appear in the shard - # matrix or be explicitly exempted — and an exempt module that gains test - # sources fails the guard until it is wired into a shard. + run: python3 scripts/check-module-list.py - name: Verify every module with tests is wired into a CI test shard - run: | - python3 - <<'PY' - import re - from pathlib import Path + run: python3 scripts/check-test-shards.py - settings = Path('settings.gradle.kts').read_text() - check = Path('.github/workflows/reusable-check.yml').read_text() - - modules = set(re.findall(r'"(:[^"]+)"', settings)) - - # Modules whose tests run in a dedicated job or only on-device -- - # exempt unconditionally. - covered_elsewhere = { - ':screenshot-tests', # dedicated screenshot-check job - ':docs-screenshots', # doc-screenshot generation (screenshot tooling) - ':baselineprofile', # benchmark module, instrumented-only - } - # Modules with no unit-test sources yet. One of these gaining test - # sources fails the guard: move it into a shard in reusable-check.yml - # and remove it from this list. - no_tests_yet = { - ':core:di', - ':marketing-screenshots', # store-listing screenshot generator, a JVM program - ':core:nfc', - ':core:resources', - } - - shards = check.split('# ── Sharded Unit Tests')[1].split('# ── Android Build')[0] - - def has_test_sources(module): - root = Path(module.lstrip(':').replace(':', '/')) - return any( - f.suffix == '.kt' - for d in root.glob('src/*') - if 'test' in d.name.lower() - for f in d.rglob('*.kt') - ) - - problems = [] - for m in sorted(modules): - if m in covered_elsewhere: - continue - if m in no_tests_yet: - if has_test_sources(m): - problems.append(f'{m} is exempt as test-less but has test sources -- wire it into a shard') - # Require an actual test task (allTests / test / testUnitTest), - # not just any reference -- a lone kover entry must not satisfy this. - elif not re.search(rf'{re.escape(m)}:(allTests|test)', shards): - problems.append(f'{m} has no test task in any reusable-check.yml test shard') - - if problems: - print('CI shard coverage drift detected:') - for p in problems: - print(' -', p) - raise SystemExit(1) - exempt = covered_elsewhere | no_tests_yet - print(f'{len(modules) - len(modules & exempt)} modules verified against the shard matrix.') - PY - - # 1c. STORE METADATA: Enforce store-listing length limits (e.g. the F-Droid / - # Play 80-char short_description). These files are mirrored from Crowdin, so - # this guard intentionally runs on the translation-sync PRs too (no - # scheduled-updates / l10n_main skip) -- that is where overlength translations + # 1c. REPO CHECKS: actionlint, shellcheck, the script self-tests, and the store-listing, + # AppStream and generated-file checks. Store listings are mirrored from Crowdin, so + # this job intentionally runs on the translation-sync PRs too (no + # scheduled-updates skip) -- that is where overlength translations # land. It is a standalone lightweight job, decoupled from the Gradle build so # a one-line translation fix never triggers a full assemble/test cycle. check-metadata: - name: Check Store Metadata + name: Check Workflows, Scripts & Metadata if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-26.04-arm timeout-minutes: 5 @@ -286,12 +129,12 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # Workflow linting (actionlint + its shellcheck integration over every run: block). - # Version-pinned; the runner image ships shellcheck. ~1s over the whole tree. + # actionlint plus its shellcheck pass over every run: block. The image bundles + # shellcheck and reads .github/actionlint.yaml from the mounted workspace. - name: Lint GitHub workflows (actionlint) - run: | - bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash) 1.7.12 /tmp - /tmp/actionlint -color + uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 + with: + args: -color - name: Lint repo shell scripts (shellcheck) # -x so sourced libraries are followed into rather than reported as SC1091, and # find rather than a glob: scripts/*.sh missed scripts/lib/, scripts/docs/ and @@ -351,6 +194,12 @@ jobs: echo "::error file=$METAINFO::Missing entry. Add it alongside the VERSION_NAME_BASE bump." exit 1 fi + # Flathub wants screenshot links from a tag or a commit, never a branch. Ours + # are the release assets the internal cut attaches, so they name the version. + if grep '' "$METAINFO" | grep -qvF "/releases/download/v$VERSION/"; then + echo "::error file=$METAINFO::Every URL must point at the v$VERSION release assets (releases/download/v$VERSION/...). Update them alongside the VERSION_NAME_BASE bump." + exit 1 + fi # Flathub runs this exact command (flatpak-builder-lint checks/metainfo.py) and turns any # non-zero exit into appstream-failed-validation at publish time. Nothing here ran it, so a @@ -393,8 +242,8 @@ jobs: # 3. WORKFLOW STATUS: Ensures required checks are satisfied # Pure gate job: no checkout, no toolchain, just reads `needs` results. It is the required - # check, so it runs on the label the rest of the workflow already gets slots on, not on - # ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. + # check, so it runs on a hosted Ubuntu label that shares the org's pool with the build jobs, + # not on ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. check-workflow-status: name: Check Workflow Status runs-on: ubuntu-26.04-arm @@ -412,12 +261,12 @@ jobs: fi if [[ "${{ needs.check-metadata.result }}" == "failure" || "${{ needs.check-metadata.result }}" == "cancelled" ]]; then - echo "::error::Store metadata length check failed" + echo "::error::Workflow, script or metadata checks failed" exit 1 fi - # If changes were detected but build failed, fail the status check - if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then + # skipped means neither the android nor the desktop filter matched + if [[ "${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled" ]]; then echo "::error::Android Check failed" exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eff31d604c..156f85abb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,32 +3,18 @@ name: Make Release on: workflow_call: inputs: - base_version: - description: 'The base version for the release (e.g., 2.3.0)' - required: true - type: string tag_name: description: 'The tag that triggered the release' required: true type: string - commit_sha: - description: 'The commit SHA to build and tag' - required: false - type: string - channel: - description: 'The channel to create a release for or promote to' + version_name: + description: 'The version name the build carries (e.g., 2.3.0)' + required: true + type: string + version_code: + description: 'The version code the build carries' required: true type: string - build_desktop: - description: 'Whether to build the desktop distribution' - required: false - type: boolean - default: false - build_flatpak_src: - description: 'Whether to build the Flatpak sources' - required: false - type: boolean - default: false secrets: GSERVICES: required: true @@ -42,6 +28,8 @@ on: required: true DATADOG_CLIENT_TOKEN: required: true + DATADOG_API_KEY: + required: false GOOGLE_MAPS_API_KEY: required: true GOOGLE_PLAY_JSON_KEY: @@ -79,64 +67,19 @@ concurrency: permissions: contents: write - pull-requests: read id-token: write attestations: write jobs: - prepare-build-info: - runs-on: ubuntu-26.04-arm - timeout-minutes: 10 - outputs: - APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} - APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.tag_name }} - fetch-depth: 0 - submodules: 'recursive' - - name: Prep APP_VERSION_NAME - id: prep_version - env: - INPUT_TAG_NAME: ${{ inputs.tag_name }} - run: | - VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//') - echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT" - echo "Parsed Version: $VERSION_NAME" - - - name: Extract VERSION_CODE_OFFSET from config.properties - id: get_version_code_offset - run: | - OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2) - echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT" - - - name: Calculate Version Code from Git Commit Count - id: calculate_version_code - env: - VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }} - run: | - COMMIT_COUNT=$(git rev-list --count HEAD) - if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then - echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'" - exit 1 - fi - VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET)) - echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT" - shell: bash - release-google: runs-on: ubuntu-26.04 - timeout-minutes: 90 - needs: [prepare-build-info] + timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.tag_name }} fetch-depth: 0 - submodules: 'recursive' - name: Gradle Setup uses: ./.github/actions/gradle-setup @@ -154,7 +97,6 @@ jobs: DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} - GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} run: | rm -f ./androidApp/google-services.json echo "$GSERVICES" > ./androidApp/google-services.json @@ -165,19 +107,26 @@ jobs: echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" } >> ./secrets.properties - echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json - - name: Setup Fastlane - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 - with: - ruby-version: '4.0.7' - bundler-cache: true - - - name: Build and Deploy Google Play to Internal Track with Fastlane + # Build only: publish-play uploads the bundle once every leg has built. The Datadog + # mapping upload runs in the same invocation so its build ID matches the bundle's. + - name: Build the Google release env: - VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} - run: bundle exec fastlane internal + VERSION_NAME: ${{ inputs.version_name }} + VERSION_CODE: ${{ inputs.version_code }} + DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} + run: | + tasks=(:androidApp:bundleGoogleRelease :androidApp:assembleGoogleRelease) + if [ -n "$DD_API_KEY" ]; then + tasks+=(:androidApp:uploadMappingGoogleRelease) + else + echo "::warning::DATADOG_API_KEY is not set, so Datadog gets no R8 mapping for this release" + fi + ./gradlew "${tasks[@]}" \ + -Pandroid.injected.version.name="$VERSION_NAME" \ + -Pandroid.injected.version.code="$VERSION_CODE" \ + -PaboutLibraries.release=true \ + -Pmeshtastic.disableAbiSplits=true - name: List outputs run: ls -R androidApp/build/outputs/ @@ -198,6 +147,19 @@ jobs: path: androidApp/build/outputs/apk/google/release/*.apk retention-days: 1 + # github-release attaches it, so anyone can retrace a pasted google-flavor stack. + - name: Compress the R8 mapping + env: + VERSION_CODE: ${{ inputs.version_code }} + run: gzip -c androidApp/build/outputs/mapping/googleRelease/mapping.txt > "mapping-google-$VERSION_CODE.txt.gz" + + - name: Upload the R8 mapping artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: google-mapping + path: mapping-google-*.txt.gz + retention-days: 1 + - name: Attest Google AAB provenance if: success() uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 @@ -212,15 +174,13 @@ jobs: release-fdroid: runs-on: ubuntu-26.04 - timeout-minutes: 90 - needs: [prepare-build-info] + timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.tag_name }} fetch-depth: 0 - submodules: 'recursive' - name: Gradle Setup uses: ./.github/actions/gradle-setup @@ -238,17 +198,15 @@ jobs: echo "$KEYSTORE" | base64 -di > "./androidApp/$KEYSTORE_FILENAME" echo "$KEYSTORE_PROPERTIES" > ./keystore.properties - - name: Setup Fastlane - uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 - with: - ruby-version: '4.0.7' - bundler-cache: true - - - name: Build F-Droid with Fastlane + # No aboutLibraries.release: offlineMode keeps the output matching F-Droid's reproducible rebuild. + - name: Build the F-Droid release env: - VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} - run: bundle exec fastlane fdroid_build + VERSION_NAME: ${{ inputs.version_name }} + VERSION_CODE: ${{ inputs.version_code }} + run: > + ./gradlew :androidApp:assembleFdroidRelease + -Pandroid.injected.version.name="$VERSION_NAME" + -Pandroid.injected.version.code="$VERSION_CODE" - name: List outputs run: ls -R androidApp/build/outputs/ @@ -268,10 +226,8 @@ jobs: subject-path: androidApp/build/outputs/apk/fdroid/release/*.apk release-desktop: - if: ${{ inputs.build_desktop }} runs-on: ${{ matrix.os }} - timeout-minutes: 90 - needs: [prepare-build-info] + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -285,7 +241,6 @@ jobs: with: ref: ${{ inputs.tag_name }} fetch-depth: 0 - submodules: 'recursive' - name: Gradle Setup uses: ./.github/actions/gradle-setup @@ -301,8 +256,8 @@ jobs: - name: Package Native Distributions env: - ORG_GRADLE_PROJECT_appVersionName: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} - VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + ORG_GRADLE_PROJECT_appVersionName: ${{ inputs.version_name }} + VERSION_CODE: ${{ inputs.version_code }} APPIMAGE_EXTRACT_AND_RUN: 1 SIGN_MACOS: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY != '' && 'true' || 'false' }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} @@ -358,33 +313,6 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - # jpackage computes the .deb Depends: line from the build host's package names. - # Ubuntu 24.04 records time_t64 transition names (libasound2t64, libpng16-16t64) - # that don't exist on Debian 12 Bookworm / Raspberry Pi OS, making the .deb - # uninstallable there (#5233 pinned ubuntu-22.04 for this; those runners retire - # 2027-04, brownouts from 2026-09). Strip the t64 suffix instead: Bookworm ships - # the plain names, and on Trixie / Ubuntu 24.04+ the t64 packages Provide them. - # Rewriting the control file is the only host-independent fix — jpackage's - # --linux-package-deps is additive-only, and the Compose plugin clears its - # --resource-dir during the task, so a custom control template can't be injected. - - name: Rewrite t64 dependency names in .deb (Debian Bookworm / Pi OS compatibility) - if: runner.os == 'Linux' - run: | - shopt -s nullglob - for deb in desktopApp/build/compose/binaries/main-release/deb/*.deb; do - work=$(mktemp -d) - dpkg-deb -R "$deb" "$work" - echo "$(basename "$deb") before: $(grep '^Depends:' "$work/DEBIAN/control")" - sed -i -E '/^(Depends|Pre-Depends|Recommends):/ s/(lib[a-z0-9.+-]*)t64([, (]|$)/\1\2/g' "$work/DEBIAN/control" - echo "$(basename "$deb") after: $(grep '^Depends:' "$work/DEBIAN/control")" - if grep -nE '^[A-Za-z-]+:.*\blib[a-z0-9.+-]*t64\b' "$work/DEBIAN/control"; then - echo "::error::t64 dependency name survived rewrite in $(basename "$deb") — extend the sed above" - exit 1 - fi - dpkg-deb -b --root-owner-group "$work" "$deb" - rm -rf "$work" - done - # CMP's TargetFormat.AppImage is jpackage's "app-image" — an unpacked # directory, not a Linux .AppImage. Wrap it into a real AppImage so # Linux users get a portable no-install binary alongside .deb/.rpm. @@ -393,7 +321,7 @@ jobs: - name: Build AppImage from jpackage app-image if: runner.os == 'Linux' env: - APP_VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + APP_VERSION_NAME: ${{ inputs.version_name }} run: scripts/build-appimage.sh - name: List Desktop Binaries @@ -430,10 +358,8 @@ jobs: desktopApp/build/compose/jars/*-release.jar create-flatpak-src: - if: ${{ inputs.build_flatpak_src }} runs-on: ${{ matrix.os }} timeout-minutes: 60 - needs: [prepare-build-info] strategy: fail-fast: false matrix: @@ -446,7 +372,6 @@ jobs: with: ref: ${{ inputs.tag_name }} fetch-depth: 0 - submodules: 'recursive' - name: Gradle Setup uses: ./.github/actions/gradle-setup @@ -484,9 +409,8 @@ jobs: retention-days: 1 release-flatpak-src: - if: ${{ inputs.build_flatpak_src }} - runs-on: ubuntu-26.04 - timeout-minutes: 30 + runs-on: ubuntu-slim + timeout-minutes: 10 needs: [create-flatpak-src] steps: - name: Download Flatpak source artifacts @@ -521,20 +445,62 @@ jobs: with: subject-path: flatpak-sources.json - github-release: - if: ${{ !cancelled() && !failure() }} + # The store-listing screenshots are an output of the tag: the real debug apps on an + # emulator and on a virtual display, connected to Demo Mode's showcase mesh + # (store-screenshots.yml), attached to the release, and published to the Play listing + # by the production promotion. A capture failure must not lose the release or its tag + # (the caller deletes the tag on a failed run): the capture runs soft, github-release + # does not gate on it, and packaging falls back to the committed sets. + store-screenshots: + uses: ./.github/workflows/store-screenshots.yml + with: + ref: ${{ inputs.tag_name }} + soft: true + secrets: inherit + + # Play receives the bundle only after every leg has built: a failed leg makes the caller + # delete the tag, and Play keeps any versionCode it has been sent. + publish-play: runs-on: ubuntu-26.04-arm timeout-minutes: 15 - needs: - - prepare-build-info - - release-google - - release-fdroid - - release-desktop - - release-flatpak-src + needs: [release-google, release-fdroid, release-desktop, release-flatpak-src] + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag_name }} + + - name: Download the Google AAB + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: google-aab + path: ${{ runner.temp }}/google-aab + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + bundler-cache: true + + - name: Decode Play Store credentials + env: + GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} + run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json + + - name: Upload to the internal track + env: + AAB: ${{ runner.temp }}/google-aab/androidApp-google-release.aab + run: bundle exec fastlane upload_internal aab:"$AAB" + + - name: Clean up credentials + if: always() + run: rm -f fastlane/play-store-credentials.json + + github-release: + runs-on: ubuntu-26.04-arm + timeout-minutes: 15 + needs: [publish-play] permissions: contents: write - id-token: write - attestations: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -546,8 +512,9 @@ jobs: with: path: ./artifacts - - name: Exclude flatpak-multisrc artifacts from release - run: rm -rf ./artifacts/flatpak-multisrc-* + # Raw captures already uploaded stay out: store-screenshots-assets packages and attaches them. + - name: Exclude intermediate artifacts from release + run: rm -rf ./artifacts/flatpak-multisrc-* ./artifacts/store-screenshots-* # Generate the notes ourselves with an explicit previous tag. GitHub's automatic # previous-release selection considers any published release, including the rolling @@ -560,7 +527,7 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} TAG: ${{ inputs.tag_name }} - TARGET: ${{ inputs.commit_sha || github.sha }} + TARGET: ${{ github.sha }} run: | # Fail the step if the listing itself fails — an empty PREV must only ever # mean "no published v* release exists yet", never a swallowed API error, @@ -594,9 +561,72 @@ jobs: uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: tag_name: ${{ inputs.tag_name }} - target_commitish: ${{ inputs.commit_sha || github.sha }} - name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}) + target_commitish: ${{ github.sha }} + name: ${{ inputs.tag_name }} (${{ inputs.version_code }}) body_path: release-notes.md files: ./artifacts/**/* draft: true prerelease: true + + # One zip per flavor laid out as fastlane's images/ folder (google feeds the Play + # listing, fdroid the committed tree F-Droid reads) and the five desktop PNGs loose, + # so metainfo.xml can point at them by name. A flavor or the desktop set with a shot + # missing is replaced whole by the committed one, never mixed. They are attached to + # the draft github-release created, so the draft never waits on the emulator. + store-screenshots-assets: + if: ${{ !cancelled() && needs.github-release.result == 'success' }} + runs-on: ubuntu-slim + timeout-minutes: 10 + continue-on-error: true + needs: [store-screenshots, github-release] + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag_name }} + + - name: Download the captures + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: store-screenshots-* + path: shots + + - name: Package the screenshots + env: + VERSION_CODE: ${{ inputs.version_code }} + run: | + mkdir -p out + echo "### Store screenshots" >> "$GITHUB_STEP_SUMMARY" + # Three form factors times five shots. + for flavor in google fdroid; do + src="shots/store-screenshots-$flavor" + count=$(find "$src" -name '*.png' 2>/dev/null | wc -l) + if [ "$count" -ne 15 ]; then + echo "::warning::$flavor captured $count of 15 shots; the committed set is attached instead." + echo "- $flavor: $count of 15 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY" + src=fastlane/metadata/android/en-US/images + else + echo "- $flavor: captured from the tag" >> "$GITHUB_STEP_SUMMARY" + fi + (cd "$src" && zip -qr "$GITHUB_WORKSPACE/out/store-listing-screenshots-$flavor-${VERSION_CODE}.zip" .) + done + src=shots/store-screenshots-desktop + count=$(find "$src" -name 'meshtastic-desktop-*.png' 2>/dev/null | wc -l) + if [ "$count" -ne 5 ]; then + echo "::warning::desktop captured $count of 5 shots; the committed set is attached instead." + echo "- desktop: $count of 5 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY" + src=desktopApp/packaging/linux/screenshots + else + echo "- desktop: captured from the tag" >> "$GITHUB_STEP_SUMMARY" + fi + cp "$src"/meshtastic-desktop-*.png out/ + + - name: Attach the screenshots to the draft release + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ inputs.tag_name }} + run: gh release upload "$TAG" out/* --clobber --repo "$REPO" diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 2c0f759da6..1d693f7268 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -58,9 +58,9 @@ jobs: # jobs could even enter the queue. Its two outputs are now sourced without a # job: cache writability is the pure expression above, and the versionCode # comes from one of two places: - # - Jobs whose artifacts ship (android-check, build-desktop, - # build-flatpak-src) check out full — blob-less — history so the build's - # GitVersionValueSource derives the real commit-count versionCode. + # - Jobs whose artifacts ship (android-check, build-desktop) check out full, + # blob-less history so the build's GitVersionValueSource derives the real + # commit-count versionCode. # - Validation-only jobs (lint, screenshot, test shards) pin VERSION_CODE # to a constant instead: their outputs are never shipped, and the real # value changes on every commit, which poisons the versionCode-dependent @@ -96,17 +96,11 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }} cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }} - # Disabled on Gradle 9.7.1: restoring an entry written by another runner crashes - # fingerprint deserialization (IsInIdeaSyncValueSource CNFE, settings-plugin - # classloader scope) and the job dies in ~10s before any test. A cross-machine - # restore is the only thing that triggers it, so it is 100% red in CI and not - # reproducible from a local store/reuse pair. Re-enable when that is fixed - # upstream; VERSION_CODE is still pinned above so entries would reuse. - cache_configuration_cache: 'false' - install_jetbrains_jdk: 'true' - - name: Lint, Analysis & KMP Smoke Compile - run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug -Pci=true --continue + - name: Spotless, Detekt & Android Lint + # The root spotlessCheck and detekt do not reach the included build-logic build. + # detektTypeResolved runs the rules that need a classpath, which plain detekt skips. + run: ./gradlew spotlessCheck detekt detektTypeResolved :build-logic:convention:spotlessCheck :build-logic:convention:detekt :build-logic:convention:validatePlugins androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug -Pci=true -Pkotlin.daemon.useFallbackStrategy=false --continue # ── Screenshot Test Validation ────────────────────────────────────── screenshot-check: @@ -133,18 +127,11 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }} cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }} - # Disabled on Gradle 9.7.1: restoring an entry written by another runner crashes - # fingerprint deserialization (IsInIdeaSyncValueSource CNFE, settings-plugin - # classloader scope) and the job dies in ~10s before any test. A cross-machine - # restore is the only thing that triggers it, so it is 100% red in CI and not - # reproducible from a local store/reuse pair. Re-enable when that is fixed - # upstream; VERSION_CODE is still pinned above so entries would reuse. - cache_configuration_cache: 'false' - name: Screenshot Test Validation # -Dorg.gradle.isolated-projects=false: AGP's screenshot plugin iterates BuildServicesRegistry at # execution time, which Isolated Projects forbids (fatal since Gradle 9.7). - run: ./gradlew :screenshot-tests:validateDebugScreenshotTest -Pci=true -Dorg.gradle.isolated-projects=false + run: ./gradlew :screenshot-tests:validateDebugScreenshotTest -Pci=true -Pkotlin.daemon.useFallbackStrategy=false -Dorg.gradle.isolated-projects=false - name: Upload screenshot diff report if: failure() @@ -198,8 +185,8 @@ jobs: # guards them against drift (every module in settings.gradle.kts with test # sources must appear here or be explicitly exempted). # shard-core: remaining core:* KMP module tests (allTests), plus kmpSmokeCompile, - # which is the only iOS compile :core:di, :core:nfc and :core:resources - # get. It lived in lint-check until a cold build there blew the 30m budget. + # which is the only iOS compile :core:di and :core:resources get. + # It lived in lint-check until a cold build there blew the 30m budget. # shard-feature: feature:* KMP module tests + :core:service # shard-app: Pure-Android/JVM tests (androidApp, desktopApp, # core:barcode, feature:widget) @@ -209,7 +196,7 @@ jobs: permissions: contents: read pull-requests: write - timeout-minutes: 45 + timeout-minutes: 35 if: inputs.run_unit_tests == true env: VERSION_CODE: 30000000 # pinned for cache stability -- see comment above @@ -286,6 +273,7 @@ jobs: :core:database:allTests :core:network:allTests :feature:widget:testDebugUnitTest + :schema-strings:test kover: >- :androidApp:koverXmlReportFdroidDebug :androidApp:koverXmlReportGoogleDebug @@ -310,15 +298,7 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }} cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }} - # Disabled on Gradle 9.7.1: restoring an entry written by another runner crashes - # fingerprint deserialization (IsInIdeaSyncValueSource CNFE, settings-plugin - # classloader scope) and the job dies in ~10s before any test. A cross-machine - # restore is the only thing that triggers it, so it is 100% red in CI and not - # reproducible from a local store/reuse pair. Re-enable when that is fixed - # upstream; VERSION_CODE is still pinned above so entries would reuse. - cache_configuration_cache: 'false' - # Shards run different task graphs; kover flips the graph again. - cache_key_suffix: ${{ matrix.shard.name }}${{ inputs.run_coverage && '-kover' || '' }} + cache_konan: 'true' - name: Run Tests & Coverage (${{ matrix.shard.name }}) run: | @@ -329,7 +309,9 @@ jobs: # CCUD tags every shard with the same `CI job=test-shards`, which is the largest # single CI cost bucket and therefore the one worth splitting. Shard identity is # otherwise only recoverable by parsing the requested task list. - ./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue \ + # -PwarningsAsErrors: together the shards compile every main and test source set, so a new Kotlin + # compiler warning fails here. + ./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true -Pkotlin.daemon.useFallbackStrategy=false -PwarningsAsErrors=true --continue \ "-Dscan.value.CI shard=${{ matrix.shard.name }}" # A test fork that dies in native code (exit 134) names no test, and the JVM's crash report is the @@ -455,12 +437,13 @@ jobs: retention-days: 7 # ── Android Build ──────────────────────────────────────────────────── - # Also generates the dependency graph — assembling both flavors resolves the widest set. + # On main it also submits the dependency graph, since assembling both flavors resolves + # the widest set. android-check: runs-on: ubuntu-26.04 # No permissions block on purpose: inherits the caller's. main grants contents: write - # (graph submit); PRs grant read (upload only). - timeout-minutes: 60 + # for the graph submit. + timeout-minutes: 30 if: inputs.run_android_build == true steps: @@ -478,8 +461,7 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }} cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }} - # main submits; PRs upload (no contents: write), dependency-graph-submit.yml finishes. - dependency_graph: ${{ github.event_name == 'pull_request' && 'generate-and-upload' || (github.ref == 'refs/heads/main' && 'generate-and-submit' || 'disabled') }} + dependency_graph: ${{ github.ref == 'refs/heads/main' && 'generate-and-submit' || 'disabled' }} - name: Tag main-branch builds as -SNAPSHOT if: github.event_name == 'push' @@ -569,7 +551,7 @@ jobs: # via allprojects{}, which Isolated Projects forbids (fatal since Gradle 9.7). The property form is # used everywhere instead of --no-isolated-projects: that flag only exists on 9.7+, so the property # survives a pin-back below 9.7 without editing this line. - run: ./gradlew androidApp:assembleFdroidDebug androidApp:assembleGoogleDebug -Pci=true --parallel --configuration-cache -Dorg.gradle.isolated-projects=false --continue + run: ./gradlew androidApp:assembleFdroidDebug androidApp:assembleGoogleDebug -Pci=true -Pkotlin.daemon.useFallbackStrategy=false --parallel --configuration-cache -Dorg.gradle.isolated-projects=false --continue # A dependency published for only some of our ABIs builds and installs cleanly and then # crashes with UnsatisfiedLinkError on the others (#7001). Compare the splits here, where @@ -603,7 +585,7 @@ jobs: permissions: contents: read pull-requests: write - timeout-minutes: 60 + timeout-minutes: 35 strategy: fail-fast: false matrix: @@ -624,7 +606,6 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }} cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }} - install_jetbrains_jdk: 'true' - name: Install dependencies for AppImage if: runner.os == 'Linux' @@ -652,8 +633,9 @@ jobs: # --no-configuration-cache while IP is still on. Flags, not -D properties — # -Dorg.gradle.isolated-projects=false is inert on windows-latest. --no-isolated-projects # needs Gradle 9.7+, so a pin-back below 9.7 has to revert this line with it. + # The daemon flag is quoted because PowerShell splits an unquoted -P argument at its first dot. run: > - ./gradlew :desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars -Pci=true + ./gradlew :desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars -Pci=true "-Pkotlin.daemon.useFallbackStrategy=false" ${{ runner.os == 'Windows' && '--no-isolated-projects --no-configuration-cache' || '' }} # CMP's TargetFormat.AppImage is jpackage's "app-image" — an unpacked directory, not diff --git a/.github/workflows/scheduled-baseline.yml b/.github/workflows/scheduled-baseline.yml index a01dbad6e1..cd58cd2b8a 100644 --- a/.github/workflows/scheduled-baseline.yml +++ b/.github/workflows/scheduled-baseline.yml @@ -5,7 +5,7 @@ on: # Once a day. Baseline-profile regeneration needs a booted emulator (~14 min) # and the profile/graphs rarely change, so this is decoupled from the frequent # firmware/hardware/translation job in scheduled-updates.yml. - - cron: '0 0 * * *' + - cron: '23 2 * * *' workflow_dispatch: # Allow manual triggering # Emulator runs take up to ~1 h; a manual dispatch overlapping the daily cron @@ -66,23 +66,22 @@ jobs: profile: pixel_6 disable-animations: true emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - # Writes androidApp/src//generated/baselineProfiles/ via the androidx.baselineprofile plugin. - # The variant is googleRelease (flavor + buildType), NOT the bare `google` flavor dir. + # Writes androidApp/src/main/generated/baselineProfiles/ via the androidx.baselineprofile plugin: + # :androidApp sets mergeIntoMain, which is what makes the fdroid flavor ship the profile too. # --no-configuration-cache: the underlying connectedGoogleNonMinifiedReleaseAndroidTest task is not # config-cache serializable (SeparateTestModuleTestData / ResolutionBackedFileCollection), and the # project enables org.gradle.configuration-cache by default — same workaround used in reusable-check.yml. # -Dorg.gradle.isolated-projects=false must accompany it: Isolated Projects implies the configuration cache, and # Gradle 9.7+ hard-errors when the cache is disabled while Isolated Projects is on. - script: ./gradlew :androidApp:generateGoogleReleaseBaselineProfile -Pci=true -Dorg.gradle.isolated-projects=false --no-configuration-cache + script: ./gradlew :androidApp:generateBaselineProfile -Pci=true -Dorg.gradle.isolated-projects=false --no-configuration-cache - name: Detect baseline profile changes id: baseline run: | outcome="${{ steps.generate_baseline.outcome }}" - # Pin the variant the Gradle task above targets: googleRelease (flavor + buildType), NOT the - # bare `google` flavor dir. Searching for any baseline-prof.txt would happily validate another - # variant's file — or, once this profile is committed, the stale one already in the checkout. - profile_dir="androidApp/src/googleRelease/generated/baselineProfiles" + # Pin the directory mergeIntoMain writes. Searching for any baseline-prof.txt would happily + # validate a file from some other source set, or the stale one already in the checkout. + profile_dir="androidApp/src/main/generated/baselineProfiles" profile="$profile_dir/baseline-prof.txt" if [ "$outcome" != "success" ]; then echo "::error::Baseline profile generation failed (outcome: $outcome)." @@ -129,7 +128,38 @@ jobs: echo "PREOF" } >> "$GITHUB_OUTPUT" + # GitHub refuses a push to the head branch of a queued PR (GH006), so a queued PR is left to land. + # Same check as scheduled-updates.yml, keyed on this workflow's branch. + - name: Check whether the scheduled PR is in the merge queue + id: merge_queue + env: + GH_TOKEN: ${{ github.token }} + run: | + query=$(cat <<'GQL' + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + pullRequests(headRefName: "scheduled-baseline", baseRefName: "main", states: OPEN, first: 50) { + nodes { number isCrossRepository isInMergeQueue } + } + } + } + GQL + ) + if ! queued=$(gh api graphql -f query="$query" -f owner="$GITHUB_REPOSITORY_OWNER" -f name="${GITHUB_REPOSITORY#*/}" \ + --jq '.data.repository.pullRequests.nodes[] | select(.isInMergeQueue and (.isCrossRepository | not)) | .number'); then + echo "::warning::Could not read the merge queue state of the scheduled PR; updating it anyway." + queued="" + fi + if [ -n "$queued" ]; then + echo "::notice::#$queued is in the merge queue; leaving its branch alone until it lands." + echo "in_queue=true" >> "$GITHUB_OUTPUT" + else + echo "in_queue=false" >> "$GITHUB_OUTPUT" + fi + - name: Create Pull Request if changes occurred + id: pr + if: steps.merge_queue.outputs.in_queue != 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} @@ -154,6 +184,26 @@ jobs: labels: | automation + # "Merge when ready", as in scheduled-updates.yml: the queue sets the merge method, a fresh push can + # clear an earlier request, and an already-mergeable PR is refused the request so it merges directly. + - name: Enable auto-merge + if: ${{ steps.merge_queue.outputs.in_queue != 'true' && (steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated') }} + env: + GH_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }} + run: | + PR_ID=$(gh pr view "$PR_NUMBER" --json id --jq .id) + QUERY=$(cat <<'GQL' + mutation($id: ID!) { + enablePullRequestAutoMerge(input: { pullRequestId: $id }) { + pullRequest { number autoMergeRequest { enabledAt } } + } + } + GQL + ) + gh api graphql -f query="$QUERY" -F id="$PR_ID" \ + || gh pr merge "$PR_NUMBER" + # Runs after the PR so graph updates still land, but turns the run red: a silently-green # baseline job let a path mismatch discard a freshly generated profile every night for weeks. - name: Fail the run if baseline generation broke diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml index 6608419f76..c18680b0d3 100644 --- a/.github/workflows/scheduled-updates.yml +++ b/.github/workflows/scheduled-updates.yml @@ -2,12 +2,16 @@ name: Scheduled Updates (Firmware, Hardware, Translations) on: schedule: - # Hourly. This job is cheap (curl + Crowdin + PR, no build) — the slow - # emulator/graph work lives in scheduled-baseline.yml on a daily cron. - - cron: '0 * * * *' + # Cheap (curl + Crowdin + PR; Gradle only when the protobufs pin moves). The emulator and + # graph work lives in scheduled-baseline.yml. + - cron: '17 */6 * * *' + # A protobufs pin change on main regenerates the schema strings without waiting for the cron. + push: + branches: [main] + paths: [gradle/libs.versions.toml] workflow_dispatch: # Allow manual triggering -# Hourly cron + manual dispatch must never stack: overlapping runs race on the +# Cron, push and manual dispatch must never stack: overlapping runs race on the # scheduled-updates branch force-push. Later runs queue (at most one pending). concurrency: group: ${{ github.workflow }} @@ -30,213 +34,27 @@ jobs: - name: Update firmware releases list id: firmware - run: | - firmware_file_path="androidApp/src/main/assets/firmware_releases.json" - temp_firmware_file="/tmp/new_firmware_releases.json" - - echo "Fetching latest firmware releases..." - http_code=$(curl -s -o "$temp_firmware_file" -w '%{http_code}' https://api.meshtastic.org/github/firmware/list || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Firmware API returned HTTP $http_code. Skipping firmware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from firmware API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_firmware_file" 2>/dev/null; then - echo "::warning::Firmware API returned invalid JSON data. Skipping firmware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from firmware API" >> "$GITHUB_OUTPUT" - else - # Drop `.pullRequests` before diffing and before writing: the API lists every open - # firmware PR, which turns over several times a day, and the app never reads the field - # (see NetworkFirmwareReleases). Keeping it churned an hourly PR carrying no real change. - if [ ! -f "$firmware_file_path" ] || ! jq --sort-keys 'del(.pullRequests)' "$temp_firmware_file" | diff -q - <(jq --sort-keys 'del(.pullRequests)' "$firmware_file_path"); then - echo "Changes detected in firmware list or local file missing. Updating $firmware_file_path." - jq 'del(.pullRequests)' "$temp_firmware_file" > "$firmware_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in firmware list." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch firmware - name: Update hardware list id: hardware - run: | - hardware_file_path="androidApp/src/main/assets/device_hardware.json" - temp_hardware_file="/tmp/new_device_hardware.json" - - echo "Fetching latest device hardware data..." - http_code=$(curl -s -o "$temp_hardware_file" -w '%{http_code}' https://api.meshtastic.org/resource/deviceHardware || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Hardware API returned HTTP $http_code. Skipping hardware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from hardware API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_hardware_file" 2>/dev/null; then - echo "::warning::Hardware API returned invalid JSON data. Skipping hardware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from hardware API" >> "$GITHUB_OUTPUT" - else - if [ ! -f "$hardware_file_path" ] || ! jq --sort-keys . "$temp_hardware_file" | diff -q - <(jq --sort-keys . "$hardware_file_path"); then - echo "Changes detected in hardware list or local file missing. Updating $hardware_file_path." - cp "$temp_hardware_file" "$hardware_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in hardware list." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch hardware - name: Update event firmware metadata id: event_firmware - run: | - event_firmware_file_path="androidApp/src/main/assets/event_firmware.json" - temp_event_firmware_file="/tmp/new_event_firmware.json" - - echo "Fetching latest event firmware metadata..." - http_code=$(curl -s --max-time 90 -o "$temp_event_firmware_file" -w '%{http_code}' https://api.meshtastic.org/resource/eventFirmware || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Event firmware API returned HTTP $http_code. Skipping event firmware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from event firmware API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_event_firmware_file" 2>/dev/null; then - echo "::warning::Event firmware API returned invalid JSON data. Skipping event firmware update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from event firmware API" >> "$GITHUB_OUTPUT" - else - if [ ! -f "$event_firmware_file_path" ] || ! jq --sort-keys . "$temp_event_firmware_file" | diff -q - <(jq --sort-keys . "$event_firmware_file_path"); then - echo "Changes detected in event firmware metadata or local file missing. Updating $event_firmware_file_path." - cp "$temp_event_firmware_file" "$event_firmware_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in event firmware metadata." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch event_firmware - name: Update device links list id: device_links - run: | - device_links_file_path="androidApp/src/main/assets/device_links.json" - temp_device_links_file="/tmp/new_device_links.json" - - echo "Fetching latest device links..." - http_code=$(curl -s --max-time 90 -o "$temp_device_links_file" -w '%{http_code}' https://api.meshtastic.org/resource/deviceLinks || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Device links API returned HTTP $http_code. Skipping device links update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from device links API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_device_links_file" 2>/dev/null; then - echo "::warning::Device links API returned invalid JSON data. Skipping device links update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from device links API" >> "$GITHUB_OUTPUT" - else - # This file is only the offline seed for the runtime cache, which refreshes - # itself from the same endpoint on a TTL. Guard against a degraded response - # (HTTP 200 but empty catalog) wiping the seed — mirrors the runtime, which - # ignores empty responses for the same reason. - link_count=$(jq -r 'if (.links | type) == "array" then (.links | length) else 0 end' "$temp_device_links_file" 2>/dev/null || echo 0) - if [ "$link_count" -eq 0 ]; then - echo "::warning::Device links API returned no links. Skipping to protect the bundled seed." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=empty links array from device links API" >> "$GITHUB_OUTPUT" - # Diff on `.links` only: the envelope carries a server-set `generatedAt` - # timestamp that changes on every response, so an hourly whole-document diff - # would open a churn PR even when the catalog is unchanged. - elif [ ! -f "$device_links_file_path" ] || ! jq --sort-keys '.links' "$temp_device_links_file" | diff -q - <(jq --sort-keys '.links' "$device_links_file_path"); then - echo "Changes detected in device links or local file missing. Updating $device_links_file_path." - # Pretty-print so the committed asset stays readable and future diffs stay - # minimal regardless of the API's response formatting. - jq . "$temp_device_links_file" > "$device_links_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in device links." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch device_links - name: Update bootloader OTA quirks id: bootloader_quirks - run: | - quirks_file_path="androidApp/src/main/assets/device_bootloader_ota_quirks.json" - temp_quirks_file="/tmp/new_device_bootloader_ota_quirks.json" - - echo "Fetching latest bootloader OTA quirks..." - http_code=$(curl -s --max-time 90 -o "$temp_quirks_file" -w '%{http_code}' https://api.meshtastic.org/resource/bootloaderOtaQuirks || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Bootloader quirks API returned HTTP $http_code. Skipping quirks update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from bootloader quirks API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_quirks_file" 2>/dev/null; then - echo "::warning::Bootloader quirks API returned invalid JSON data. Skipping quirks update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from bootloader quirks API" >> "$GITHUB_OUTPUT" - else - # This file is the offline seed for the runtime cache. Its softDeviceVariants - # table gates a destructive flash and must fail closed, so a degraded response - # (HTTP 200 but an empty table) must never wipe the seed. - variant_count=$(jq -r 'if (.softDeviceVariants | type) == "array" then (.softDeviceVariants | length) else 0 end' "$temp_quirks_file" 2>/dev/null || echo 0) - if [ "$variant_count" -eq 0 ]; then - echo "::warning::Bootloader quirks API returned no softDeviceVariants. Skipping to protect the bundled seed." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=empty softDeviceVariants from bootloader quirks API" >> "$GITHUB_OUTPUT" - elif [ ! -f "$quirks_file_path" ] || ! jq --sort-keys . "$temp_quirks_file" | diff -q - <(jq --sort-keys . "$quirks_file_path"); then - echo "Changes detected in bootloader quirks or local file missing. Updating $quirks_file_path." - jq . "$temp_quirks_file" > "$quirks_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in bootloader quirks." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch bootloader_quirks - name: Update maintenance UF2 manifest id: maintenance_uf2 - run: | - uf2_file_path="androidApp/src/main/assets/maintenance_uf2.json" - temp_uf2_file="/tmp/new_maintenance_uf2.json" - - echo "Fetching latest maintenance UF2 manifest..." - http_code=$(curl -s --max-time 90 -o "$temp_uf2_file" -w '%{http_code}' https://api.meshtastic.org/resource/maintenanceUf2 || true) - http_code="${http_code:-0}" - - if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then - echo "::warning::Maintenance UF2 API returned HTTP $http_code. Skipping manifest update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=HTTP $http_code from maintenance UF2 API" >> "$GITHUB_OUTPUT" - elif ! jq empty "$temp_uf2_file" 2>/dev/null; then - echo "::warning::Maintenance UF2 API returned invalid JSON data. Skipping manifest update." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=Invalid JSON response from maintenance UF2 API" >> "$GITHUB_OUTPUT" - else - # The manifest's digest-pinned images gate destructive maintenance flashes; - # a degraded response (missing erase set or empty board map) must never - # replace the bundled seed. - board_count=$(jq -r 'if (.otafixByBoardId | type) == "object" then (.otafixByBoardId | length) else 0 end' "$temp_uf2_file" 2>/dev/null || echo 0) - # .erase.nrf52Bootloader is required too: the bundled seed carries it, and a manifest without it - # would silently drop the bootloader-driven erase path from every offline install. - has_erase=$(jq -r 'if (.erase | type) == "object" and (.erase.nrf52Bootloader | type) == "object" then 1 else 0 end' "$temp_uf2_file" 2>/dev/null || echo 0) - if [ "$board_count" -eq 0 ] || [ "$has_erase" -ne 1 ]; then - echo "::warning::Maintenance UF2 API response is missing the erase set (incl. erase.nrf52Bootloader) or board map. Skipping to protect the bundled seed." - echo "status=error" >> "$GITHUB_OUTPUT" - echo "detail=degraded maintenance UF2 manifest (no erase set, no erase.nrf52Bootloader, or empty board map)" >> "$GITHUB_OUTPUT" - elif [ ! -f "$uf2_file_path" ] || ! jq --sort-keys . "$temp_uf2_file" | diff -q - <(jq --sort-keys . "$uf2_file_path"); then - echo "Changes detected in maintenance UF2 manifest or local file missing. Updating $uf2_file_path." - jq . "$temp_uf2_file" > "$uf2_file_path" - echo "status=updated" >> "$GITHUB_OUTPUT" - else - echo "No changes detected in maintenance UF2 manifest." - echo "status=unchanged" >> "$GITHUB_OUTPUT" - fi - fi + run: scripts/scheduled-updates/assets.sh fetch maintenance_uf2 - name: Refresh Obtainium configs and deep links id: obtainium @@ -264,8 +82,84 @@ jobs: git checkout -- README.md docs/en/developer/test-builds.md obtainium fi + # values/schema_strings.xml is generated from the org.meshtastic:protobufs pin and records which one. Gradle + # runs here only when main's pin has moved past the recorded one and the scheduled-updates branch does not + # already carry the file for that pin, so the job stays cheap between bumps and while the PR waits; + # the regenerated English goes up to Crowdin in the step below and rides this PR with the translations. + - name: Check whether the schema strings lag the protobufs pin + id: schema_pin + run: | + xml=core/resources/src/commonMain/composeResources/values/schema_strings.xml + strings=core/resources/src/commonMain/composeResources/values/strings.xml + recorded_pin() { sed -n 's/.*from org\.meshtastic:protobufs \([^ ]*\)\..*/\1/p' | head -n1; } + xml_header() { sed '//q'; } + catalog=$(sed -n 's/^meshtastic-protobufs = "\(.*\)"$/\1/p' gradle/libs.versions.toml) + recorded=$(recorded_pin < "$xml") + echo "catalog=$catalog" >> "$GITHUB_OUTPUT" + echo "recorded=$recorded" >> "$GITHUB_OUTPUT" + if [ "$catalog" = "$recorded" ]; then + echo "schema strings already reflect protobufs $catalog" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "schema strings reflect protobufs $recorded, catalog has $catalog" + echo "changed=true" >> "$GITHUB_OUTPUT" + branch_xml="$RUNNER_TEMP/scheduled-updates.schema_strings.xml" + # The file is a function of the pin, the generator in schema-strings/ and the strings.xml header it + # borrows, so a copy built before either of the last two changed on main is regenerated. + if git fetch --no-tags --depth=1 --quiet origin scheduled-updates 2>/dev/null \ + && git show "FETCH_HEAD:$xml" > "$branch_xml" 2>/dev/null \ + && [ "$(recorded_pin < "$branch_xml")" = "$catalog" ] \ + && [ "$(git rev-parse FETCH_HEAD:schema-strings 2>/dev/null)" = "$(git rev-parse HEAD:schema-strings)" ] \ + && [ "$(xml_header < "$branch_xml")" = "$(xml_header < "$strings")" ]; then + echo "scheduled-updates already carries the schema strings for protobufs $catalog; reusing them" + echo "reuse=$branch_xml" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Gradle Setup + if: steps.schema_pin.outputs.changed == 'true' && steps.schema_pin.outputs.reuse == '' + uses: ./.github/actions/gradle-setup + with: + gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + job_summary_pr_comment: 'never' + cache_read_only: 'true' + cache_robolectric: 'false' + + - name: Regenerate the schema strings + id: schema_strings + if: steps.schema_pin.outputs.changed == 'true' + env: + RECORDED: ${{ steps.schema_pin.outputs.recorded }} + CATALOG: ${{ steps.schema_pin.outputs.catalog }} + REUSE: ${{ steps.schema_pin.outputs.reuse }} + run: | + xml=core/resources/src/commonMain/composeResources/values/schema_strings.xml + cp "$xml" /tmp/schema_strings.before.xml + # Only the xml is taken from the branch: strings-index.txt also lists main's hand-written strings, + # which may have moved since the branch was built, so sort-strings.py rebuilds it either way. + if [ -n "$REUSE" ]; then + regenerate=(cp "$REUSE" "$xml") + else + regenerate=(./gradlew :schema-strings:sync -Pci=true) + fi + if "${regenerate[@]}" && python3 scripts/sort-strings.py; then + git clone --filter=blob:none --quiet https://github.com/meshtastic/protobufs.git /tmp/protobufs + python3 scripts/protobufs-bump-summary.py "$RECORDED" "$CATALOG" --protobufs /tmp/protobufs \ + --before /tmp/schema_strings.before.xml --after "$xml" > /tmp/schema-summary.md || true + cat /tmp/schema-summary.md >> "$GITHUB_STEP_SUMMARY" + detail=$(sed -n 's/^\*\*\([0-9]* [a-z]* strings\)\*\*.*/\1/p' /tmp/schema-summary.md | paste -sd, - | sed 's/,/, /g') + echo "status=updated" >> "$GITHUB_OUTPUT" + echo "detail=${detail:-no string changes}" >> "$GITHUB_OUTPUT" + else + echo "::warning::Schema strings sync failed — see the error above." + echo "status=error" >> "$GITHUB_OUTPUT" + echo "detail=./gradlew :schema-strings:sync failed for protobufs $CATALOG" >> "$GITHUB_OUTPUT" + git checkout -- "$xml" .skills/compose-ui/strings-index.txt + fi + - name: Sync with Crowdin - uses: crowdin/github-action@9af557de76d70c480f88065d336f445a362f402b # v3 + uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0 with: base_url: 'https://meshtastic.crowdin.com/api/v2' config: 'crowdin.yml' @@ -273,18 +167,21 @@ jobs: upload_sources: true upload_translations: false download_translations: true - create_pull_request: false - commit_message: 'chore(l10n): New Crowdin Translations from scheduled update' push_translations: false push_sources: false - localization_branch_name: ${{ github.ref_name }} + # Runs the container as the owner of .git, so the downloaded files are not root-owned. + user: 'auto' env: GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - name: Fix file permissions - run: sudo chown -R "$USER:$USER" . + # Crowdin translates layout and nav_order along with the prose and carries over the English + # parent. This restores the first two from docs/en and drops parent; docs-quality.yml fails + # the PR on any left, so a failure here must not stop the PR opening. + - name: Restore locale docs front matter + continue-on-error: true + run: python3 scripts/docs/sync-locale-front-matter.py # Early warning for store-listing translations just pulled from Crowdin # (overlength text, unmapped locale codes, forbidden HTML). Non-blocking on @@ -295,6 +192,40 @@ jobs: continue-on-error: true run: python3 scripts/check-store-metadata.py + # Each label fires a labeler run, so a category is labelled only when this run changed it. + - name: Pick the PR labels + id: labels + env: + FIRMWARE_STATUS: ${{ steps.firmware.outputs.status }} + EVENT_FIRMWARE_STATUS: ${{ steps.event_firmware.outputs.status }} + MAINTENANCE_UF2_STATUS: ${{ steps.maintenance_uf2.outputs.status }} + HARDWARE_STATUS: ${{ steps.hardware.outputs.status }} + DEVICE_LINKS_STATUS: ${{ steps.device_links.outputs.status }} + BOOTLOADER_QUIRKS_STATUS: ${{ steps.bootloader_quirks.outputs.status }} + SCHEMA_STRINGS_STATUS: ${{ steps.schema_strings.outputs.status }} + run: | + any_updated() { + for status in "$@"; do [ "$status" = updated ] && return 0; done + return 1 + } + labels=automation + if any_updated "$FIRMWARE_STATUS" "$EVENT_FIRMWARE_STATUS" "$MAINTENANCE_UF2_STATUS"; then + labels+=,firmware + fi + if any_updated "$HARDWARE_STATUS" "$DEVICE_LINKS_STATUS" "$BOOTLOADER_QUIRKS_STATUS"; then + labels+=,hardware + fi + # Crowdin reports no status, so its downloads are read off the tree: the locale copies, never the English sources. + translations=$(git status --porcelain --untracked-files=all -- \ + ':(glob)**/composeResources/values-*/**' \ + ':(glob)fastlane/metadata/android/**' ':(exclude,glob)fastlane/metadata/android/en-US/**' \ + ':(glob)docs/**' ':(exclude,glob)docs/en/**') + if [ -n "$translations" ] || any_updated "$SCHEMA_STRINGS_STATUS"; then + labels+=,l10n + fi + echo "Labels: $labels" + echo "labels=$labels" >> "$GITHUB_OUTPUT" + - name: Build PR body id: pr_body env: @@ -314,95 +245,45 @@ jobs: MAINTENANCE_UF2_DETAIL: ${{ steps.maintenance_uf2.outputs.detail }} OBTAINIUM_STATUS: ${{ steps.obtainium.outputs.status }} OBTAINIUM_DETAIL: ${{ steps.obtainium.outputs.detail }} + SCHEMA_PIN_CHANGED: ${{ steps.schema_pin.outputs.changed }} + SCHEMA_PIN_CATALOG: ${{ steps.schema_pin.outputs.catalog }} + SCHEMA_STRINGS_STATUS: ${{ steps.schema_strings.outputs.status }} + SCHEMA_STRINGS_DETAIL: ${{ steps.schema_strings.outputs.detail }} + run: scripts/scheduled-updates/assets.sh body + + # GitHub refuses a push to the head branch of a queued PR (GH006), so a queued PR is left to land. + # gh pr view --json does not expose isInMergeQueue; GraphQL does. + # headRefName also matches fork PRs; only one open same-repository PR can exist per head and base. + - name: Check whether the scheduled PR is in the merge queue + id: merge_queue + env: + GH_TOKEN: ${{ github.token }} run: | - firmware_status="$FIRMWARE_STATUS" - firmware_detail="$FIRMWARE_DETAIL" - hardware_status="$HARDWARE_STATUS" - hardware_detail="$HARDWARE_DETAIL" - event_firmware_status="$EVENT_FIRMWARE_STATUS" - event_firmware_detail="$EVENT_FIRMWARE_DETAIL" - device_links_status="$DEVICE_LINKS_STATUS" - device_links_detail="$DEVICE_LINKS_DETAIL" - bootloader_quirks_status="$BOOTLOADER_QUIRKS_STATUS" - bootloader_quirks_detail="$BOOTLOADER_QUIRKS_DETAIL" - maintenance_uf2_status="$MAINTENANCE_UF2_STATUS" - maintenance_uf2_detail="$MAINTENANCE_UF2_DETAIL" - obtainium_status="$OBTAINIUM_STATUS" - obtainium_detail="$OBTAINIUM_DETAIL" - - body="This PR includes automated updates from the scheduled workflow:" - body+=$'\n' - - # Firmware status - case "$firmware_status" in - updated) body+=$'\n'"- ✅ \`firmware_releases.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`firmware_releases.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`firmware_releases.json\` skipped — ${firmware_detail}." ;; - *) body+=$'\n'"- ❓ \`firmware_releases.json\` — unknown status." ;; - esac - - # Hardware status - case "$hardware_status" in - updated) body+=$'\n'"- ✅ \`device_hardware.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`device_hardware.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`device_hardware.json\` skipped — ${hardware_detail}." ;; - *) body+=$'\n'"- ❓ \`device_hardware.json\` — unknown status." ;; - esac - - # Event firmware status - case "$event_firmware_status" in - updated) body+=$'\n'"- ✅ \`event_firmware.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`event_firmware.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`event_firmware.json\` skipped — ${event_firmware_detail}." ;; - *) body+=$'\n'"- ❓ \`event_firmware.json\` — unknown status." ;; - esac - - # Device links status - case "$device_links_status" in - updated) body+=$'\n'"- ✅ \`device_links.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`device_links.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`device_links.json\` skipped — ${device_links_detail}." ;; - *) body+=$'\n'"- ❓ \`device_links.json\` — unknown status." ;; - esac - - # Bootloader OTA quirks status - case "$bootloader_quirks_status" in - updated) body+=$'\n'"- ✅ \`device_bootloader_ota_quirks.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`device_bootloader_ota_quirks.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`device_bootloader_ota_quirks.json\` skipped — ${bootloader_quirks_detail}." ;; - *) body+=$'\n'"- ❓ \`device_bootloader_ota_quirks.json\` — unknown status." ;; - esac - - # Maintenance UF2 manifest status - case "$maintenance_uf2_status" in - updated) body+=$'\n'"- ✅ \`maintenance_uf2.json\` updated from the Meshtastic API." ;; - unchanged) body+=$'\n'"- ✔️ \`maintenance_uf2.json\` checked — no changes detected." ;; - error) body+=$'\n'"- ⚠️ \`maintenance_uf2.json\` skipped — ${maintenance_uf2_detail}." ;; - *) body+=$'\n'"- ❓ \`maintenance_uf2.json\` — unknown status." ;; - esac - - # Obtainium configs / deep links - case "$obtainium_status" in - updated) body+=$'\n'"- ✅ Obtainium deep links and import files regenerated (channel status changed)." ;; - unchanged) body+=$'\n'"- ✔️ Obtainium configs checked against the live releases — no changes detected." ;; - error) body+=$'\n'"- ⚠️ Obtainium configs skipped — ${obtainium_detail}." ;; - *) body+=$'\n'"- ❓ Obtainium configs — unknown status." ;; - esac - - # Crowdin (always attempted) - body+=$'\n'"- Source strings were uploaded to Crowdin." - body+=$'\n'"- Latest translations were downloaded from Crowdin (if available)." - body+=$'\n' - body+=$'\n'"Please review the changes." - - # Write multi-line body to output - { - echo "content<> "$GITHUB_OUTPUT" + query=$(cat <<'GQL' + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + pullRequests(headRefName: "scheduled-updates", baseRefName: "main", states: OPEN, first: 50) { + nodes { number isCrossRepository isInMergeQueue } + } + } + } + GQL + ) + if ! queued=$(gh api graphql -f query="$query" -f owner="$GITHUB_REPOSITORY_OWNER" -f name="${GITHUB_REPOSITORY#*/}" \ + --jq '.data.repository.pullRequests.nodes[] | select(.isInMergeQueue and (.isCrossRepository | not)) | .number'); then + echo "::warning::Could not read the merge queue state of the scheduled PR; updating it anyway." + queued="" + fi + if [ -n "$queued" ]; then + echo "::notice::#$queued is in the merge queue; leaving its branch alone until it lands." + echo "in_queue=true" >> "$GITHUB_OUTPUT" + else + echo "in_queue=false" >> "$GITHUB_OUTPUT" + fi - name: Create Pull Request if changes occurred + id: pr + if: steps.merge_queue.outputs.in_queue != 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} @@ -417,6 +298,7 @@ jobs: - Crowdin source string uploads - Crowdin translation downloads - Obtainium deep links and import files + - Schema strings regenerated from the protobufs pin title: 'chore: Scheduled updates (Firmware, Hardware, Translations)' body: ${{ steps.pr_body.outputs.content }} branch: 'scheduled-updates' @@ -431,11 +313,33 @@ jobs: androidApp/src/main/assets/maintenance_uf2.json fastlane/metadata/android/** **/strings.xml + **/schema_strings.xml + .skills/compose-ui/strings-index.txt docs/**/*.md README.md obtainium/** - labels: | - automation - l10n - firmware - hardware + labels: ${{ steps.labels.outputs.labels }} + + # "Merge when ready": the merge queue takes the PR once its checks pass. + # No merge method is passed; the queue sets it. Re-run on every update, + # since a fresh push can clear a previous request. GitHub refuses the + # request on a PR that is already mergeable, so that case merges directly. + # A failure fails the run: the PR exists either way, and a request that + # cannot be made is something to fix, not to hide. + - name: Enable auto-merge + if: ${{ steps.merge_queue.outputs.in_queue != 'true' && (steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated') }} + env: + GH_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }} + run: | + PR_ID=$(gh pr view "$PR_NUMBER" --json id --jq .id) + QUERY=$(cat <<'GQL' + mutation($id: ID!) { + enablePullRequestAutoMerge(input: { pullRequestId: $id }) { + pullRequest { number autoMergeRequest { enabledAt } } + } + } + GQL + ) + gh api graphql -f query="$QUERY" -F id="$PR_ID" \ + || gh pr merge "$PR_NUMBER" diff --git a/.github/workflows/store-screenshots.yml b/.github/workflows/store-screenshots.yml new file mode 100644 index 0000000000..6f2b985583 --- /dev/null +++ b/.github/workflows/store-screenshots.yml @@ -0,0 +1,197 @@ +name: Store Screenshots + +# Captures the store-listing screenshots from the real debug apps, connected to Demo +# Mode's hidden showcase mesh. Android runs `:store-screenshots` once per flavor, each on +# its own emulator: the google flavor feeds the Play listing, the fdroid flavor the fastlane +# tree F-Droid and IzzyOnDroid read, each uploaded as `store-screenshots-` laid +# out as `images/Screenshots/_.png`. Desktop runs the real app on a virtual +# display (`store-screenshots/capture-desktop.sh`) and uploads the five Flathub PNGs as +# `store-screenshots-desktop`. +# +# release.yml calls this on every internal cut. It also runs on a pull request that +# touches the renderer or the showcase mesh, so a change is seen on an emulator +# before it reaches a release. + +on: + workflow_call: + inputs: + ref: + description: 'The ref to capture (the release tag)' + required: true + type: string + soft: + # A caller cannot mark a reusable workflow allowed-to-fail, and a failed job in + # release.yml deletes the internal tag. With soft, a failed capture fails nothing. + description: 'Let a failed capture pass without failing the calling run' + required: false + type: boolean + default: false + workflow_dispatch: + pull_request: + branches: [ main ] + paths: + - 'store-screenshots/**' + - 'core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockScenario.kt' + - '.github/workflows/store-screenshots.yml' + +permissions: + contents: read + +# A called workflow's github.workflow is the caller's name, so a prefix built from it +# equals release.yml's own group on the same tag and GitHub cancels the call as a deadlock. +concurrency: + group: store-screenshots-${{ github.event.pull_request.number || inputs.ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + android: + name: Android (${{ matrix.flavor }}) + # Hosted x64 runners expose KVM; the emulator needs it. + runs-on: ubuntu-26.04 + timeout-minutes: 35 + continue-on-error: ${{ inputs.soft == true }} + strategy: + fail-fast: false + matrix: + include: + - flavor: google + variant: Google + - flavor: fdroid + variant: Fdroid + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.ref || github.sha }} + fetch-depth: 0 + + - name: Gradle Setup + uses: ./.github/actions/gradle-setup + with: + gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + cache_read_only: 'true' + + # The google flavor draws Google Maps, which needs the debug Maps key; the fdroid + # flavor draws MapLibre and needs nothing. + - name: Provide the debug Maps key + if: ${{ matrix.flavor == 'google' }} + env: + GOOGLE_MAPS_API_KEY_DEBUG: ${{ secrets.GOOGLE_MAPS_API_KEY_DEBUG }} + run: | + if [ -n "$GOOGLE_MAPS_API_KEY_DEBUG" ]; then + echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY_DEBUG" >> ./google.properties + else + echo "::warning::GOOGLE_MAPS_API_KEY_DEBUG is unset -- the google-flavor map shot will be blank." + fi + + # Built before the emulator boots, so the emulator step only installs and runs, with + # no second Gradle configuration. + - name: Build the app and the capture module + run: ./gradlew :androidApp:assemble${{ matrix.variant }}Debug :store-screenshots:assemble${{ matrix.variant }}Debug + + - name: Enable KVM (for the emulator) + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + # The runner's `script:` runs each line in its own shell, so the capture is one script. + - name: Capture on the emulator + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + profile: pixel_6 + cores: 4 + disable-animations: true + emulator-options: -no-window -gpu swiftshader -noaudio -no-boot-anim -camera-back none + script: store-screenshots/capture-android.sh ${{ matrix.flavor }} out + + - name: Summarize the captures + if: ${{ always() }} + env: + FLAVOR: ${{ matrix.flavor }} + run: | + { + echo "### Store screenshots (${FLAVOR})" + if [ -n "$(find "out/$FLAVOR" -name '*.png' 2>/dev/null)" ]; then + find "out/$FLAVOR" -name '*.png' | sort | sed "s|^out/$FLAVOR/|- |" + else + echo "- none captured" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the captures + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: store-screenshots-${{ matrix.flavor }} + path: out/${{ matrix.flavor }} + if-no-files-found: warn + retention-days: 7 + + # The failing test's exception is in instrument.txt, the rest of the story in logcat.txt. + - name: Upload the test results + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: store-screenshots-test-results-${{ matrix.flavor }} + path: out/test-results/${{ matrix.flavor }} + if-no-files-found: warn + retention-days: 7 + + # The five Flathub shots, from the real desktop debug build on a virtual display. + desktop: + name: Desktop + runs-on: ubuntu-26.04 + timeout-minutes: 20 + continue-on-error: ${{ inputs.soft == true }} + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.ref || github.sha }} + fetch-depth: 0 + + - name: Gradle Setup + uses: ./.github/actions/gradle-setup + with: + gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + cache_read_only: 'true' + + - name: Build the desktop app + run: ./gradlew :desktopApp:createDistributable + + # Software GL through zink over lavapipe, because Skiko refuses llvmpipe by name. + - name: Install a virtual display and software GL + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq xvfb openbox xdotool imagemagick \ + libvulkan1 mesa-vulkan-drivers libgl1 libglx-mesa0 libegl1 libgl1-mesa-dri + + - name: Capture on a virtual display + run: | + store-screenshots/capture-desktop.sh \ + "desktopApp/build/compose/binaries/main/app/Meshtastic Desktop/bin/Meshtastic Desktop" \ + out/desktop + + - name: Summarize the captures + if: ${{ always() }} + run: | + { + echo "### Store screenshots (desktop)" + find out/desktop -name '*.png' 2>/dev/null | sort | sed 's|^out/desktop/|- |' || echo "- none captured" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the captures + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: store-screenshots-desktop + path: out/desktop + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index c50a301ecf..4cabbf8aa3 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -1,14 +1,14 @@ name: Update Changelog -# Manual only: promote.yml stamps the released section at release time on its -# own. Dispatch this to refresh the [Unreleased] section between releases. +# Create or Promote Release dispatches this after every internal, closed and open run; +# a production promotion stamps the released section itself. Dispatch it by hand to +# refresh the [Unreleased] section at any other time. on: workflow_dispatch: +# The PR is opened and queued with CROWDIN_GITHUB_TOKEN through .github/actions/bot-pr. permissions: contents: write - pull-requests: write - statuses: write concurrency: group: changelog-${{ github.ref }} @@ -266,41 +266,13 @@ jobs: - name: Create or update changelog PR if: steps.tags.outputs.prod != '' && steps.update.outputs.changed == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - BRANCH="automation/update-changelog" - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Force-update the automation branch - git checkout -B "$BRANCH" - git add CHANGELOG.md - git commit -m "docs: update CHANGELOG.md" - git push origin "$BRANCH" --force - - # Create or update the PR - EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number') - if [ -n "$EXISTING_PR" ]; then - echo "Updated existing PR #$EXISTING_PR" - else - gh pr create \ - --title "docs: update CHANGELOG.md" \ - --body "Automated changelog refresh, dispatched from main." \ - --head "$BRANCH" \ - --base main \ - --label "automation" \ - --label "skip-changelog" - echo "Created new changelog PR" - fi - - # Post the required "Check Workflow Status" commit status so the PR - # isn't blocked. PRs from GITHUB_TOKEN don't trigger pull_request - # workflows, so the normal CI never runs. CHANGELOG-only PRs don't - # need CI checks. - COMMIT_SHA=$(git rev-parse HEAD) - gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \ - -f state="success" \ - -f context="Check Workflow Status" \ - -f description="Skipped — changelog-only PR" + uses: ./.github/actions/bot-pr + with: + token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + branch: automation/update-changelog + title: 'docs: update CHANGELOG.md' + body: 'Automated changelog refresh, dispatched from main.' + add-paths: CHANGELOG.md + labels: | + automation + skip-changelog diff --git a/.github/workflows/verify-flatpak.yml b/.github/workflows/verify-flatpak.yml index 685ed57802..441cdf8656 100644 --- a/.github/workflows/verify-flatpak.yml +++ b/.github/workflows/verify-flatpak.yml @@ -8,29 +8,17 @@ on: paths: - 'scripts/verify-flatpak/**' - '.github/workflows/verify-flatpak.yml' - # The dependency surface the manifest captures is verified post-merge. This is not a - # required check and never ran in the merge queue, so per-PR it cost ~2 runner slots - # for a signal that blocks nothing; post-merge still catches a break within one merge, - # and `main` itself was previously never verified at all. + # Post-merge only for what the check itself depends on. The offline manifest pins the + # Gradle distribution apart from the wrapper, so a wrapper bump is checked on merge. push: branches: [ main ] paths: - 'scripts/verify-flatpak/**' - '.github/workflows/verify-flatpak.yml' - - 'build.gradle.kts' - - 'settings.gradle.kts' - # The desktop module's build config shapes the uber jar the flatpak wraps. - - 'desktopApp/**' - # The offline manifest pins the Gradle distribution independently of the wrapper — - # a wrapper bump without a manifest update breaks the offline build silently. - 'gradle/wrapper/**' - # build.gradle.kts reads compose-multiplatform from the catalog (#6911), so a - # catalog-only bump changes the manifest's platform URLs. Deliberately the whole - # file and not a key filter: a filter naming today's keys goes stale silently the - # moment the manifest reads another one — the failure #6911 existed to remove. - - 'gradle/libs.versions.toml' - # Drift no path filter can see: a Flathub runtime bump, or an upstream artifact that - # moved or vanished. Nothing in this repo changes, so no other trigger would fire. + # The dependency surface the manifest captures (desktopApp/**, the version catalog, the + # root build scripts) is verified nightly, as is drift no path filter can see: a + # Flathub runtime bump, or an upstream artifact that moved or vanished. schedule: - cron: '0 4 * * *' workflow_dispatch: @@ -60,8 +48,6 @@ jobs: fail-fast: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive # Renovate mirrors wrapper bumps into the manifest's distribution URL but cannot # rewrite its sha256, so on a wrapper bump this drift is expected: fail here in @@ -106,7 +92,7 @@ jobs: if: steps.setup-jbr.outcome == 'failure' run: echo "::warning::JBR setup-java failed; falling back to Foojay toolchain provisioning." - - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6 with: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} @@ -121,9 +107,11 @@ jobs: echo "### Flatpak Sources Summary (${{ matrix.arch }})" >> "$GITHUB_STEP_SUMMARY" echo "- URLs captured: $(jq length flatpak-sources.json)" >> "$GITHUB_STEP_SUMMARY" - # flatpak-builder re-downloads the whole closure anyway; a second copy just eats disk. - - name: Reclaim the isolated Gradle home - run: rm -rf "$RUNNER_TEMP/flatpak-gradle-home" + # The sandbox build below starts its own Gradle and Kotlin daemons. The pair + # this step left running holds the other half of the runner's memory, and the + # VM stalls and is shut down mid-compile. Stop them; the home itself can stay. + - name: Stop the outer Gradle daemon + run: ./gradlew -Dgradle.user.home="$RUNNER_TEMP/flatpak-gradle-home" --stop - name: Clone the Flathub packaging repo run: | diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000000..793475ea68 --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,105 @@ +name: Bump Version Name + +# Opens the next patch line once a version ships to production, through a PR that +# merges itself: VERSION_NAME_BASE, its AppStream entry and the Play +# what's-new, all written by scripts/bump-version-name.py. +# +# promote.yml flips the release to production with GITHUB_TOKEN, whose events start no +# workflows, so it dispatches this one explicitly, as it does winget-publish.yml. The +# release trigger covers a release published by hand; workflow_dispatch is the retry path. +on: + release: + types: [released] + workflow_dispatch: + inputs: + tag: + description: 'Production release tag that shipped (e.g., v2.8.2)' + required: true + type: string + +# The PR is opened and queued with CROWDIN_GITHUB_TOKEN through .github/actions/bot-pr. +permissions: + contents: read + pull-requests: read + +# A release event overlapping a dispatch would open two PRs for the same version. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + bump: + if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && !github.event.release.prerelease && !github.event.release.draft }} + runs-on: ubuntu-slim + timeout-minutes: 10 + env: + TAG: ${{ inputs.tag || github.event.release.tag_name }} + steps: + # A release event checks out the tag by default; the bump branches from main. + - name: Checkout main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + + - name: Resolve next version + id: next + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "::error::'$TAG' is not a production tag (vX.Y.Z)." + exit 1 + fi + SHIPPED="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))" + BRANCH="automation/version-name-${NEXT}" + CURRENT=$(sed -n 's/^VERSION_NAME_BASE=//p' config.properties) + + # A hand bump that already landed (a minor line, say) wins over the patch default. + if [[ "$CURRENT" != "$SHIPPED" && "$(printf '%s\n%s\n' "$CURRENT" "$SHIPPED" | sort -V | tail -1)" == "$CURRENT" ]]; then + echo "::notice::main is already on $CURRENT, past $SHIPPED; nothing to bump." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + OPEN=$(gh pr list --state open --limit 200 --json number,title,headRefName \ + | jq -r --arg b "$BRANCH" --arg n "$NEXT" \ + '.[] | select(.headRefName != $b and (.title | test("VERSION_NAME_BASE to " + ($n | gsub("[.]"; "[.]")) + "([^0-9.]|$)"))) | .number') + if [[ -n "$OPEN" ]]; then + echo "::notice::A hand bump to $NEXT is already open: #$OPEN." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + { + echo "skip=false" + echo "shipped=$SHIPPED" + echo "next=$NEXT" + echo "branch=$BRANCH" + } >> "$GITHUB_OUTPUT" + + - name: Bump + if: ${{ steps.next.outputs.skip == 'false' }} + env: + NEXT: ${{ steps.next.outputs.next }} + run: python3 scripts/bump-version-name.py "$NEXT" + + # Creates the PR, or updates it on a retry. Pushed with the PAT, so pull-request.yml's + # AppStream and what's-new gates run on it like any other bump. + - name: Open the bump PR + if: ${{ steps.next.outputs.skip == 'false' }} + uses: ./.github/actions/bot-pr + with: + token: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + title: 'chore: bump VERSION_NAME_BASE to ${{ steps.next.outputs.next }}' + body: | + Opens the ${{ steps.next.outputs.next }} line now that v${{ steps.next.outputs.shipped }} has shipped. + + The metainfo `` for ${{ steps.next.outputs.next }} is a placeholder. Rewrite it and re-run `python3 scripts/sync-play-changelog.py` before the ${{ steps.next.outputs.next }} internal cut, or it ships as the release Highlights and the Play what's-new. + branch: ${{ steps.next.outputs.branch }} + add-paths: | + config.properties + desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml + fastlane/metadata/android/en-US/changelogs/default.txt + labels: | + automation + skip-changelog diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml index 994a3cd290..b07fbefb80 100644 --- a/.github/workflows/winget-publish.yml +++ b/.github/workflows/winget-publish.yml @@ -43,6 +43,13 @@ jobs: # the token is configured. HAS_WINGET_TOKEN: ${{ secrets.WINGET_TOKEN != '' && 'true' || 'false' }} steps: + # A skipped submission must not read as a published one. + - name: Report an unconfigured winget publish + if: env.HAS_WINGET_TOKEN != 'true' + run: | + echo "::warning::WINGET_TOKEN is not set; nothing was submitted to microsoft/winget-pkgs." + echo "- winget: skipped, WINGET_TOKEN not set" >> "$GITHUB_STEP_SUMMARY" + # Prerequisites (both manual, one-time): # 1. Meshtastic.MeshtasticDesktop must already exist in # microsoft/winget-pkgs — winget-releaser only updates existing diff --git a/.gitignore b/.gitignore index 93f6820717..2cc8cdf00f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ .cxx /app/release **/debug/** +!/androidApp/src/debug/** **/release/** # Java KeyStore certificates @@ -53,7 +54,6 @@ wireless-install.sh docs/_site/ docs/.jekyll-cache/ docs/.jekyll-metadata -docs/Gemfile.lock # Git worktrees .worktrees/ @@ -70,6 +70,9 @@ hs_err_pid*.log replay_pid*.log .attach_pid* +# Local build output redirected to a file +/build.log + # Local library development clones /coil/ /kable/ @@ -82,8 +85,6 @@ feature/docs/src/commonMain/composeResources/files/*/docs/ /desktop/bin/ /build-logic/convention/bin/ /.specify/extensions/.cache/ -# Jekyll local config (comments out remote_theme for local builds) -docs/_config_local.yml # Flatpak source manifests and repositories flatpak-sources-*.json diff --git a/.skills/ci-cost-control/SKILL.md b/.skills/ci-cost-control/SKILL.md index b50718862f..f17829557f 100644 --- a/.skills/ci-cost-control/SKILL.md +++ b/.skills/ci-cost-control/SKILL.md @@ -1,3 +1,8 @@ +--- +name: ci-cost-control +description: Keep GitHub Actions spend down on Meshtastic-Android - avoid redundant runs, cancel superseded ones, and pick the narrowest CI path for the change. Use this whenever you are about to push repeatedly, re-run a workflow, touch `.github/workflows/`, or someone asks why CI is slow, queued or expensive. +--- + # Skill: CI Cost Control & Monitoring ## Description @@ -15,7 +20,7 @@ gh run list --branch $(git branch --show-current) --limit 5 ### 2. Local First NEVER use CI as a "remote compiler." -- You must run `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests` locally before pushing. +- You must run `./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests` locally before pushing. - If local tests fail, CI **will** fail. Do not waste the tokens or the compute. ### 3. Let the path filters do their job diff --git a/.skills/code-review/SKILL.md b/.skills/code-review/SKILL.md index 86cc1922a4..cd41000640 100644 --- a/.skills/code-review/SKILL.md +++ b/.skills/code-review/SKILL.md @@ -1,3 +1,8 @@ +--- +name: code-review +description: Review a Meshtastic-Android change against KMP architecture, Compose Multiplatform and Modern Android Development conventions, starting from the four defect classes that keep recurring because neither the compiler, detekt nor spotless can see them. Use this for any PR review, for self-review before pushing, and whenever asked whether a change is safe to merge. +--- + # Skill: Code Review ## Description @@ -58,7 +63,7 @@ When reviewing code, meticulously verify the following categories. Flag any devi - `java.util.concurrent.ConcurrentHashMap` -> `atomicfu` or Mutex-guarded `mutableMapOf()` - `java.io.*` -> `Okio` (`BufferedSource`/`BufferedSink`) - `java.util.Locale` -> Kotlin `uppercase()`/`lowercase()` (purged from `commonMain`) -- [ ] **Coroutine Safety:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` silently swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction). Use `kotlinx.coroutines.CancellationException` (not `kotlin.coroutines.cancellation.CancellationException`). +- [ ] **Coroutine Safety:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` silently swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction). Use `kotlinx.coroutines.CancellationException` (not `kotlin.coroutines.cancellation.CancellationException`). `detektTypeResolved` enforces the catch side through `SuspendFunSwallowedCancellation`: a generic catch around a suspend call needs `catch (e: CancellationException) { throw e }` before it, or `if (e is CancellationException) currentCoroutineContext().ensureActive()` as its first line where a stray cancellation must not end the caller. A konsist rule enforces the import. - [ ] **Shared Helpers:** If `androidMain` and `jvmMain` contain identical pure-Kotlin logic, mandate extracting it to a shared function in `commonMain`. - [ ] **File Naming Conflicts:** For `expect`/`actual` declarations, ensure files sharing the same package namespace have distinct names (e.g., keep `expect` in `LogExporter.kt` and shared helpers in `LogFormatter.kt`) to avoid duplicate class errors on the JVM target. - [ ] **Interface & DI Over `expect`/`actual`:** Check that `expect`/`actual` is reserved for small platform primitives. Interfaces + DI should be preferred for larger capabilities. @@ -68,7 +73,7 @@ When reviewing code, meticulously verify the following categories. Flag any devi - [ ] **String Formatting:** CMP only supports `%N$s` and `%N$d`. Flag any float formats (`%N$.1f`) in Compose string resources; they must be pre-formatted using `NumberFormatter.format()` from `core:common`. Use `MetricFormatter` for metric-specific displays (temperature, voltage, current, percent, humidity, pressure, SNR, RSSI). - [ ] **Centralized Dialogs & Alerts:** Flag inline alert-rendering logic. Mandate the use of `AlertHost(alertManager)` or `SharedDialogs` from `core:ui/commonMain`. - [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. No inline placeholders in feature modules. -- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktopApp/tablet breakpoints (≥ 1200dp). +- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfoV2()`, which includes the desktopApp/tablet Large and XL width classes (≥ 1200dp); `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` is deprecated in its favour. ### 3. Navigation & State - [ ] **Shared Navigation Graphs:** Feature navigation graphs must be defined as extension functions on `EntryProviderScope` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(...)`). Flag any graphs defined in platform-specific source sets. diff --git a/.skills/compose-ui/SKILL.md b/.skills/compose-ui/SKILL.md index 9c6886c1ec..b5ba893d43 100644 --- a/.skills/compose-ui/SKILL.md +++ b/.skills/compose-ui/SKILL.md @@ -1,12 +1,18 @@ +--- +name: compose-ui +description: Build shared Compose Multiplatform UI in Meshtastic-Android - adaptive layouts on Material 3 Adaptive, plus the string and resource rules. Use this whenever you add or change a composable, add a user-facing string, or work on tablet, desktop or landscape layout. Consult the bundled `strings-index.txt` rather than opening the raw `strings.xml`, which is guarded for size. +--- + # Skill: Compose Multiplatform (CMP) UI ## Description Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive. ## 1. UI Components & Layouts -- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets. +- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfoV2()`, which includes the Large (1200dp) and XL (1600dp) width classes; `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` is deprecated in its favour. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets. - **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups. - **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. +- **Empty states:** Use `EmptyState(icon, title, supportingText, action)` from `core:ui/commonMain` for an empty list or pane rather than a hand-built icon-and-text column. - **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`. - **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `androidApp` or `desktopApp` shells. Do not tightly couple Google Maps dependencies to `commonMain`; the MapLibre surfaces live in `:feature:map-maplibre`, not in a `core` module. @@ -44,6 +50,15 @@ Choose the right tool for the job: 2. Run `python3 scripts/sort-strings.py` — keeps the file sorted and regenerates `strings-index.txt`. 3. Use the generated `org.meshtastic.core.resources.` symbol. 4. Validate UI presentation. +- **Schema strings are generated, not written.** Every label and description in the protobufs field metadata is + in `values/schema_strings.xml`, keyed by schema path: `Config.LoRaConfig.hop_limit` is + `Res.string.schema_lora_hop_limit`, its summary `schema_lora_hop_limit_description`, the enum value + `PositionFlags.DOP` `schema_position_positionflags_dop` (all indexed under `### SCHEMA` in `strings-index.txt`). + A control that edits one whole schema field uses that key; a control that edits a bit, a threshold, a negation or + drops a unit keeps a hand-written string. Never edit the generated file or write a `schema_` key by hand; + `:schema-strings:test` fails on both. Wrong wording is a `protobufs` change. A merged protobufs pin bump + triggers a `scheduled-updates` run on main that regenerates the file (it records the pin it was built from); run + `./gradlew :schema-strings:sync` yourself only when you need a new key before that PR lands. ## 3. Tooling & Capabilities - **Image Loading:** Use `libs.coil` (Coil Compose) in feature modules. Configuration/Networking for Coil (`coil-network-ktor3`) happens strictly in the `androidApp` and `desktopApp` host modules. diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index b00eef67ae..c202f13db9 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -1,16 +1,19 @@ ### A11Y ### +a11y_humidity a11y_label_value a11y_message_from a11y_node_battery +a11y_node_channel a11y_node_distance_away a11y_node_favorite -a11y_node_hops_away +a11y_node_hops_count a11y_node_last_heard a11y_node_offline a11y_node_online a11y_node_role a11y_node_signal a11y_nodes_at_hop +a11y_temperature about accept acknowledgements @@ -28,7 +31,6 @@ action_toggle_translation action_translate_message actions adc_multiplier_override -adc_multiplier_override_ratio adc_voltage ### ADD ### add @@ -42,7 +44,6 @@ add_network_device add_network_device_manually add_network_layer address -admin_key admin_keys administration advanced @@ -52,24 +53,14 @@ advanced_title air_quality air_quality_icon air_quality_metrics_log -air_quality_metrics_module_enabled -air_quality_metrics_update_interval_seconds air_util_definition air_utilization -### ALERT ### -alert_bell_buzzer -alert_bell_led alert_bell_text -alert_bell_vibra -alert_message_buzzer -alert_message_led -alert_message_vibra all_time allow_input_source allow_undefined_pin_access alt altitude -always_point_north ambient_lighting ambient_lighting_config ammonium @@ -120,8 +111,6 @@ backup_keys backup_keys_confirmation backup_restore bad -### BANDWIDTH ### -bandwidth bandwidth_default bandwidth_option_khz bandwidth_unsupported @@ -131,15 +120,12 @@ battery battery_ina_2xx_i2c_address biochemical_oxygen_demand ble_devices -ble_rssi_threshold_defaults_to_80 ble_scan_needs_location_services -blue ### BLUETOOTH ### bluetooth bluetooth_available_devices bluetooth_config bluetooth_disabled -bluetooth_enabled bluetooth_feature_config bluetooth_feature_config_description bluetooth_feature_discovery @@ -157,16 +143,12 @@ bluetooth_scan_location_services_disabled bluetooth_scan_missing_permission bluetooth_scan_start_failed bluetooth_scan_too_frequent -bold_heading bonding_failed_permissions bonding_failed_retry boot_reconnect_blocked_message boot_reconnect_blocked_title bottom_nav_settings -broadcast_interval busy_noise_floor -button_gpio -buzzer_gpio cache_limit_eviction_warning calculating call_sign @@ -181,7 +163,6 @@ canned_message_config canned_message_enabled cant_change_no_radio cant_shutdown -carousel_interval ch_util_definition ### CHANNEL ### channel @@ -195,7 +176,6 @@ channel_7 channel_8 channel_features channel_invalid -channel_label channel_name channel_url channel_utilization @@ -214,6 +194,7 @@ chirpy_error_token_budget_exceeded chirpy_error_unknown chirpy_error_unsupported_flavor chirpy_error_unsupported_platform +chirpy_fallback_unavailable chirpy_intro chirpy_ready chirpy_search_placeholder @@ -241,9 +222,6 @@ close_selection co2 co2_humidity co2_temperature -codec_2_enabled -codec2_sample_rate -coding_rate collapse_chart collapsed communicate_off_the_grid @@ -256,49 +234,28 @@ compass_no_location_fix compass_no_location_permission compass_no_magnetometer compass_north_top -compass_orientation compass_title compass_uncertainty compass_uncertainty_unknown compromised_keys ### CONFIG ### -config_device_doubleTapAsButtonPress_summary config_device_ledHeartbeatEnabled_summary -config_device_transmitOverLora_summary config_device_tripleClickAsAdHocPing_summary config_device_tzdef_summary config_device_use_phone_tz -config_display_auto_screen_carousel_secs_summary -config_display_compass_north_top_summary -config_display_displaymode_summary -config_display_flip_screen_summary -config_display_heading_bold_summary -config_display_oled_summary -config_display_screen_on_secs_summary -config_display_units_summary -config_display_wake_on_tap_or_motion_summary -config_lora_frequency_slot_summary -config_lora_hop_limit_summary +config_lora_coding_rate_fraction +config_lora_coding_rate_override +config_lora_coding_rate_override_max_summary +config_lora_coding_rate_override_summary +config_lora_coding_rate_preset_default config_lora_modem_preset_licensed_summary config_lora_modem_preset_summary config_lora_region_summary -config_network_eth_enabled_summary config_network_udp_enabled_summary -config_network_wifi_enabled_summary -config_position_broadcast_secs_summary -config_position_broadcast_smart_minimum_distance_summary -config_position_broadcast_smart_minimum_interval_secs_summary -config_position_flags_summary -config_position_gps_update_interval_summary -config_power_is_power_saving_summary config_saved_restarting -config_security_admin_key -config_security_debug_log_api_enabled -config_security_is_managed config_security_private_key config_security_private_key_remote config_security_public_key -config_security_serial_enabled configuration configure_bluetooth_permissions configure_critical_alerts @@ -348,7 +305,6 @@ debug_filter_clear debug_filter_included debug_filter_preset_title debug_filters -debug_log_api_enabled debug_logcat_empty debug_logcat_refresh debug_logs_export @@ -396,8 +352,6 @@ desktop_update_download details detection_sensor detection_sensor_config -detection_sensor_enabled -detection_trigger_type ### DEVICE ### device device_configuration @@ -412,17 +366,15 @@ device_metrics_label_value device_metrics_log device_metrics_numeric_value device_metrics_percent_value -device_metrics_update_interval_seconds device_metrics_voltage_value device_sleeping device_storage_ui_title -device_telemetry_enabled -device_telemetry_enabled_summary device_theme_language dew_point direct_message direct_message_key direct_messages +disabled discard_changes disconnect disconnected @@ -438,6 +390,7 @@ discovery_dwell_progress discovery_dwell_time discovery_dwell_time_description discovery_empty_history +discovery_empty_history_hint discovery_export_report discovery_history discovery_interrupted_scan_restored @@ -500,15 +453,11 @@ discovery_stat_unselected discovery_stop_scan discovery_summary_not_available discovery_time_remaining -discovery_unique_nodes +discovery_unique_nodes_count discovery_view_map disk_free_indexed -### DISPLAY ### display display_config -display_mode -display_time_in_12h_format -display_units dissolved_oxygen ### DISTANCE ### distance @@ -516,9 +465,10 @@ distance_filters distance_filters_description distance_measurements distance_measurements_description -dns ### DOC ### +doc_auto_translated doc_clear_search +doc_community_translated doc_keywords_app_functions doc_keywords_connections doc_keywords_debug_logs @@ -532,6 +482,7 @@ doc_keywords_messages doc_keywords_mqtt doc_keywords_node_metrics doc_keywords_nodes +doc_keywords_notifications doc_keywords_onboarding doc_keywords_settings_module doc_keywords_settings_radio @@ -542,8 +493,12 @@ doc_keywords_translate doc_keywords_units doc_keywords_widget doc_loading +doc_no_content doc_no_documentation doc_no_results +doc_open_page +doc_page_not_found +doc_page_not_found_detail doc_search_placeholder doc_section_developer doc_section_user @@ -560,6 +515,7 @@ doc_title_messages doc_title_mqtt doc_title_node_metrics doc_title_nodes +doc_title_notifications doc_title_onboarding doc_title_settings_module doc_title_settings_radio @@ -572,15 +528,19 @@ doc_title_widget documentation done dont_show_again_for_device -double_tap_as_button_press downlink_enabled downlink_feature_description download download_this_area duplicated_public_key_title +### DURATION ### +duration_ago +duration_days_short +duration_hours_short +duration_minutes_short +duration_seconds_short dynamic easily_set_up_private_mesh_networks -echo_enabled edit edit_custom_tile_source eight_hours @@ -599,28 +559,19 @@ emoji_category_travel_places emoji_load_error emoji_no_results emoji_recently_used -enable_power_saving_mode enabled -### ENCRYPTION ### -encryption_enabled encryption_error encryption_error_text encryption_pkc encryption_pkc_text env_metrics_log -### ENVIRONMENT ### environment -environment_metrics_module_enabled -environment_metrics_on_screen_enabled -environment_metrics_update_interval_seconds -environment_metrics_use_fahrenheit error error_duty_cycle error_recovery_exhausted establish_session establishing_session ethernet_config -ethernet_enabled ethernet_ip event_firmware_running event_use_event_theme @@ -636,7 +587,7 @@ export_node_db export_tak_data_package external_notification external_notification_config -external_notification_enabled +external_power_short factory_reset fair fallback_node_name @@ -667,6 +618,11 @@ firmware firmware_edition firmware_event_ended_banner firmware_event_ended_button +firmware_maintenance_bootloader_available +firmware_maintenance_bootloader_installed +firmware_maintenance_bootloader_latest_hint +firmware_maintenance_bootloader_up_to_date +firmware_maintenance_bootloader_up_to_date_hint firmware_maintenance_cdc_unblock_failed firmware_maintenance_copy_failed firmware_maintenance_data_unavailable @@ -761,6 +717,8 @@ firmware_update_success_wiped firmware_update_taking_a_while firmware_update_target firmware_update_title +firmware_update_transfer_percent +firmware_update_transfer_progress firmware_update_unknown_error firmware_update_unknown_hardware firmware_update_unknown_release @@ -781,9 +739,7 @@ firmware_update_wipe_confirm_warning firmware_update_wipe_label firmware_update_wipe_usb_detail firmware_version -fixed_pin fixed_position -flip_screen for_more_information_see_our_privacy_policy ### FORMAT ### format_bold @@ -795,13 +751,7 @@ fr_HT free_memory free_memory_description freq -frequency_slot -friendly_name gas_resistance -gateway -generate_input_event_on_ccw -generate_input_event_on_cw -generate_input_event_on_press generate_qr_code ### GEOFENCE ### geofence @@ -826,32 +776,18 @@ geofence_set_area get_started github_repository good -### GPIO ### gpio gpio_pin -gpio_pin_for_rotary_encoder_a_port -gpio_pin_for_rotary_encoder_b_port -gpio_pin_for_rotary_encoder_press_port -gpio_pin_to_monitor -gps_en_gpio -gps_mode -gps_receive_gpio -gps_transmit_gpio grant_permission -green ham_long_name_summary hardware hardware_model heading -heartbeat help_and_documentation hide_layer hide_password -history_return_max -history_return_window hop_histogram_empty hop_histogram_title -hop_limit hops_away host host_metrics_log @@ -859,18 +795,13 @@ humidity i_agree i_agree_to_share_my_location i_know_what_i_m_doing -i2s_clock -i2s_data_in -i2s_data_out -i2s_word_select iaq iaq_definition +iaq_value icon_meanings -### IGNORE ### ignore ignore_add ignore_incoming -ignore_mqtt ignore_remove ### IMPORT ### import_configuration @@ -898,7 +829,6 @@ intro_welcome ip ip_address ip_port -ipv4_mode json_output_enabled jump_to_latest_from jump_to_latest_from_and_more @@ -911,6 +841,7 @@ key_backup_saved key_verification_final_title key_verification_request_title key_verification_title +label_with_unit last_heard_filter_label last_position_update latest_alpha_firmware @@ -923,8 +854,6 @@ layer_type_geojson layer_type_kml layer_type_network learn_more -led_heartbeat -led_state legacy_admin_channel library_count license @@ -974,6 +903,8 @@ location_permission_blocked_notice location_permission_blocked_toast location_permission_denied_notice location_permission_rationale +location_precise_blocked_toast +location_precise_rationale location_sharing ### LOCKDOWN ### lockdown_backoff @@ -1017,6 +948,8 @@ long_name longitude lora lora_config +lora_config_change_value +lora_config_changes low_battery low_battery_message low_battery_title @@ -1024,13 +957,10 @@ low_entropy_key_title lux manage_custom_tile_sources manage_map_layers -managed_mode manual_position_request ### MAP ### map -map_cache_info map_cache_manager -map_cache_megabytes map_cache_size map_cache_tiles map_clear_tiles @@ -1051,6 +981,8 @@ map_filter_show_ignored map_filter_title map_layer_formats map_layer_opacity +map_layer_open_failed +map_layer_too_large map_node_popup_details map_offline_banner map_offline_download_failed_build @@ -1073,11 +1005,8 @@ map_overlay_precipitation map_overlay_weather_radar map_purge_fail map_purge_success -map_reporting map_reporting_consent_header map_reporting_consent_text -map_reporting_interval_seconds -map_reporting_summary map_select_download_region map_start_download map_style_selection @@ -1108,7 +1037,6 @@ mesh_beacon_interval_error mesh_beacon_invitations_title mesh_beacon_listen mesh_beacon_listen_summary -mesh_beacon_message mesh_beacon_message_error mesh_beacon_no_channels mesh_beacon_notification_body @@ -1140,15 +1068,30 @@ mesh_map_location_description ### MESHTASTIC ### meshtastic meshtastic_alerts_notifications +meshtastic_alerts_notifications_description meshtastic_app_name meshtastic_broadcast_notifications +meshtastic_broadcast_notifications_description +meshtastic_client_notifications +meshtastic_client_notifications_description +meshtastic_device_status_notifications +meshtastic_device_status_notifications_description meshtastic_low_battery_notifications +meshtastic_low_battery_notifications_description meshtastic_low_battery_temporary_remote_notifications +meshtastic_low_battery_temporary_remote_notifications_description meshtastic_mesh_beacon_notifications +meshtastic_mesh_beacon_notifications_description meshtastic_messages_notifications +meshtastic_messages_notifications_description meshtastic_new_nodes_notifications +meshtastic_new_nodes_notifications_description +meshtastic_reactions_notifications +meshtastic_reactions_notifications_description meshtastic_service_notifications +meshtastic_service_notifications_description meshtastic_waypoints_notifications +meshtastic_waypoints_notifications_description ### MESSAGE ### message message_delivery_status @@ -1185,10 +1128,12 @@ message_routing_error_rate_limit_exceeded_detail message_routing_error_timeout_detail message_routing_error_too_large message_routing_error_too_large_detail +message_status_ack_proof_invalid message_status_delivered message_status_enroute message_status_queued message_status_recipient_delivered +message_status_recipient_delivered_proven message_status_relayed_not_confirmed message_status_sfpp_confirmed message_status_sfpp_routing @@ -1198,9 +1143,6 @@ messages metric_channel_label micrograms_per_cubic_meter min -minimum_broadcast_seconds -minimum_distance -minimum_interval minimum_wake_time_seconds ### MIRROR ### mirror_active @@ -1229,7 +1171,6 @@ mpwrd_os ### MQTT ### mqtt mqtt_config -mqtt_enabled mqtt_error_connection_lost mqtt_error_credentials_rejected mqtt_error_proxy_failed @@ -1252,6 +1193,8 @@ mqtt_status_disconnected_with_reason mqtt_status_inactive mqtt_status_reconnecting mqtt_status_reconnecting_with_attempt +mqtt_status_topics_refused_all +mqtt_status_topics_refused_some mqtt_test_connection must_set_region must_update @@ -1267,7 +1210,6 @@ mute_status_always mute_status_muted_for_days mute_status_muted_for_hours mute_status_unmuted -nag_timeout_seconds name name_cannot_be_empty navigate_back @@ -1277,7 +1219,6 @@ need_hardware need_hardware_description neighbor_info neighbor_info_config -neighbor_info_enabled network network_layer_url_hint new_channel_rcvd @@ -1293,7 +1234,6 @@ no_bluetooth_devices_hint no_bluetooth_devices_seen no_custom_tile_sources_found no_device_selected -no_devices_found no_files_manifested no_local_stats no_map_layers_loaded @@ -1366,7 +1306,6 @@ node_sort_title node_sort_via_favorite node_sort_via_mqtt nodedb_reset -nodeinfo_broadcast_interval ### NODES ### nodes nodes_at_this_location @@ -1388,10 +1327,13 @@ not_connected not_now note notes +### NOTIFICATION ### +notification_group_mesh notification_permission_blocked_notice notification_permission_denied_notice notification_permission_rationale notification_permissions_description +notification_reaction_to ### NOTIFICATIONS ### notifications_for_channel_and_direct_messages notifications_for_low_battery_alerts @@ -1399,8 +1341,6 @@ notifications_for_newly_discovered_nodes notifications_on_alert_bell_receipt notifications_on_message_receipt now -ntp_server -number_of_records ### OFFLINE ### offline_maps offline_maps_empty @@ -1408,9 +1348,7 @@ offline_terrain_cache_detail offline_terrain_empty offline_terrain_manager offline_terrain_regional_detail -ok_to_mqtt okay -oled_type ### ONE ### one_day one_hour @@ -1430,17 +1368,8 @@ open_wifi_settings options orient_north orp -### OUTPUT ### -output_buzzer_gpio -output_duration_milliseconds -output_led_active_high -output_led_gpio -output_vibra_gpio overflow_menu override_console_serial_port -override_duty_cycle -override_frequency_mhz -pa_fan_disabled ### PACKET ### packet_authenticity packet_authenticity_balanced @@ -1454,7 +1383,6 @@ packet_authenticity_strict_confirmation packet_authenticity_strict_summary packet_authenticity_strict_title packet_authenticity_unsupported -pairing_mode password ### PAX ### pax @@ -1467,7 +1395,6 @@ pax_wifi_format pax_wifi_marker paxcounter paxcounter_config -paxcounter_enabled periodic_position_broadcast ### PERMISSION ### permission_camera_summary @@ -1479,6 +1406,7 @@ permission_missing_pre31 permission_nearby_devices_summary permission_notifications_summary permission_state_allowed +permission_state_approximate_only permission_state_blocked permission_state_denied permission_state_not_applicable @@ -1496,6 +1424,7 @@ phosphorus pin_selected play plurals_hours +plurals_milliseconds plurals_minutes plurals_seconds ### PM ### @@ -1515,17 +1444,12 @@ pm2_5 position position_config_set_fixed_from_phone position_enabled -position_flags position_log position_packet potassium -### POWER ### power power_config power_metrics_log -power_metrics_module_enabled -power_metrics_on_screen_enabled -power_metrics_update_interval_seconds powered ppm precise_location @@ -1548,14 +1472,11 @@ preview_text primary primary_channel_feature privacy_url -private_key project_information provide_location_to_mesh provider_name_exists -proxy_to_client_enabled psk pt_BR -ptt_pin public_key public_key_changed qr_code @@ -1573,26 +1494,17 @@ rainfall_1h rainfall_24h range_test range_test_config -range_test_enabled react reboot -### REBROADCAST ### -rebroadcast_mode -rebroadcast_mode_all_desc -rebroadcast_mode_all_skip_decoding_desc -rebroadcast_mode_core_portnums_only_desc -rebroadcast_mode_known_only_desc -rebroadcast_mode_local_only_desc -rebroadcast_mode_none_desc +reboot_into_dfu +reboot_into_dfu_warning recent_network_devices reconnecting -red redacted refresh refresh_metadata regenerate_keys_confirmation regenerate_private_key -region_frequency_plan relays ### REMOTE ### remote @@ -1641,32 +1553,17 @@ ringtone_imported role role_client role_client_base -role_client_base_desc -role_client_desc role_client_hidden -role_client_hidden_desc role_client_mute -role_client_mute_desc role_lost_and_found -role_lost_and_found_desc role_repeater -role_repeater_desc role_router role_router_client -role_router_client_desc -role_router_desc role_router_late -role_router_late_desc role_sensor -role_sensor_desc role_tak -role_tak_desc role_tak_tracker -role_tak_tracker_desc role_tracker -role_tracker_desc -root_topic -rotary_encoder_1_enabled router_role_confirmation_text ### ROUTING ### routing_error_admin_bad_session_key @@ -1689,15 +1586,12 @@ routing_error_timeout routing_error_too_large rssi rssi_definition -rsyslog_server salinity sample_message sats -### SAVE ### save save_and_restart save_changes -save_csv_in_storage_esp32_only save_rangetest ### SCAN ### scan @@ -1711,7 +1605,6 @@ scan_shared_contact_nfc scan_shared_contact_qr scanning_bluetooth scanning_network -screen_on_for scroll_to_bottom search_emoji search_messages @@ -1720,6 +1613,12 @@ secondary_channel_position_feature secondary_no_telemetry ### SECURITY ### security +security_ack_proof_failed +security_ack_proof_failed_info +security_ack_proof_no_key +security_ack_proof_no_key_info +security_ack_proof_verified +security_ack_proof_verified_info security_icon_badge_warning_description security_icon_description security_icon_help_dismiss @@ -1750,25 +1649,16 @@ select_all selected selected_map_type send -send_bell -send_bell_with_alert_message -sender_message_interval_seconds -### SERIAL ### serial -serial_baud_rate serial_config -serial_console -serial_enabled -serial_mode -serial_rx_pin -serial_tx_pin -server session_active session_refresh_required set_time set_up_connection set_your_region settings +settings_search_no_results +settings_search_placeholder ### SHARE ### share share_action @@ -1802,7 +1692,6 @@ show_translation show_waypoints shutdown shutdown_node_name -shutdown_on_power_loss shutdown_warning signal signal_quality @@ -1836,7 +1725,6 @@ site_planner_use_node_location site_planner_webview_updating skip slot -smart_position snr snr_definition soil_moisture @@ -1847,39 +1735,24 @@ solar_irradiance speed speed_kmh speed_mph -spread_factor -ssid -state_broadcast_seconds status_message status_message_summary stay_connected_anywhere stop_connecting store_forward store_forward_config -store_forward_enabled -subnet success super_deep_sleep_duration_seconds supported supported_by_community +supported_by_maker swipe_action_delete swipe_action_mute swipe_action_unmute -sx126x_rx_boosted_gain system_settings ### TAK ### tak tak_config -tak_role -tak_role_forwardobserver -tak_role_hq -tak_role_k9 -tak_role_medic -tak_role_rto -tak_role_sniper -tak_role_teamlead -tak_role_teammember -tak_role_unspecified tak_server tak_server_channel tak_server_channel_desc @@ -1911,22 +1784,6 @@ tak_server_test_results_v2 tak_server_test_run tak_server_test_running tak_server_v1_fallback_notice -tak_team -tak_team_blue -tak_team_brown -tak_team_cyan -tak_team_dark_blue -tak_team_dark_green -tak_team_green -tak_team_magenta -tak_team_maroon -tak_team_orange -tak_team_purple -tak_team_red -tak_team_teal -tak_team_unspecified_color -tak_team_white -tak_team_yellow telemetry telemetry_config temperature @@ -1935,10 +1792,9 @@ theme_dark theme_light theme_system time -time_zone timeout timestamp -tls_enabled +tls_enabled_public_broker_summary toggle_my_position trace_route ### TRACEROUTE ### @@ -1987,7 +1843,6 @@ translation_model_download_failed translation_not_required transmit_disabled transmit_disabled_summary -transmit_over_lora ### TRANSPORT ### transport transport_api @@ -2002,12 +1857,13 @@ turbidity twenty_four_hours two_days two_weeks -tx_enabled -tx_power_dbm type type_a_message udp_enabled undo +unit_dbm +unit_khz +unit_meters ### UNITS ### units units_follow_system @@ -2028,9 +1884,7 @@ unmute_selected unpin_selected unrecognized unset -up_down_select_input_enabled -update_interval -update_interval_seconds +unsupported_device update_status updated uplink_enabled @@ -2039,19 +1893,14 @@ uptime ### URL ### url url_cannot_be_empty +url_http_localhost_only url_must_be_http url_must_contain_placeholders url_template url_template_hint usb usb_permission_denied -### USE ### -use_12h_format use_homoglyph_characters_encoding -use_i2s_as_buzzer -use_input_pullup_mode -use_modem_preset -use_pwm_buzzer ### USER ### user user_config @@ -2059,7 +1908,6 @@ user_id user_info user_string userinfo -username uv_lux via_api via_mqtt @@ -2067,8 +1915,6 @@ via_udp view_on_map view_release voltage -wait_for_bluetooth_duration_seconds -wake_on_tap_or_motion warning water_ph waypoint_delete @@ -2082,7 +1928,6 @@ what_is_meshtastic_description ### WIFI ### wifi_config wifi_devices -wifi_enabled wifi_ip wifi_provision_available_networks wifi_provision_connect_failed @@ -2120,7 +1965,6 @@ wifi_provision_success_username_value wifi_provisioning wifi_qr_code_error wifi_qr_code_scan -wifi_rssi_threshold_defaults_to_80 wifi_unavailable ### WIND ### wind @@ -2139,3 +1983,530 @@ zh_CN zh_TW zoom_in zoom_out +### SCHEMA (generated, values/schema_strings.xml) ### +schema_ambientlighting_blue +schema_ambientlighting_blue_description +schema_ambientlighting_current +schema_ambientlighting_current_description +schema_ambientlighting_green +schema_ambientlighting_green_description +schema_ambientlighting_led_state +schema_ambientlighting_led_state_description +schema_ambientlighting_red +schema_ambientlighting_red_description +schema_audio_audio_baud_codec2_1200 +schema_audio_audio_baud_codec2_1300 +schema_audio_audio_baud_codec2_1400 +schema_audio_audio_baud_codec2_1600 +schema_audio_audio_baud_codec2_2400 +schema_audio_audio_baud_codec2_3200 +schema_audio_audio_baud_codec2_450 +schema_audio_audio_baud_codec2_700c +schema_audio_audio_baud_codec2_default +schema_audio_bitrate +schema_audio_bitrate_description +schema_audio_codec2_enabled +schema_audio_codec2_enabled_description +schema_audio_i2s_din +schema_audio_i2s_sck +schema_audio_i2s_sd +schema_audio_i2s_ws +schema_audio_ptt_pin +schema_audio_ptt_pin_description +schema_bluetooth_enabled +schema_bluetooth_enabled_description +schema_bluetooth_fixed_pin +schema_bluetooth_fixed_pin_description +schema_bluetooth_mode +schema_bluetooth_mode_description +schema_bluetooth_pairingmode_fixed_pin +schema_bluetooth_pairingmode_no_pin +schema_bluetooth_pairingmode_random_pin +schema_cannedmessage_inputbroker_event_ccw +schema_cannedmessage_inputbroker_event_ccw_description +schema_cannedmessage_inputbroker_event_cw +schema_cannedmessage_inputbroker_event_cw_description +schema_cannedmessage_inputbroker_event_press +schema_cannedmessage_inputbroker_event_press_description +schema_cannedmessage_inputbroker_pin_a +schema_cannedmessage_inputbroker_pin_a_description +schema_cannedmessage_inputbroker_pin_b +schema_cannedmessage_inputbroker_pin_b_description +schema_cannedmessage_inputbroker_pin_press +schema_cannedmessage_inputbroker_pin_press_description +schema_cannedmessage_inputeventchar_back +schema_cannedmessage_inputeventchar_cancel +schema_cannedmessage_inputeventchar_down +schema_cannedmessage_inputeventchar_left +schema_cannedmessage_inputeventchar_none +schema_cannedmessage_inputeventchar_right +schema_cannedmessage_inputeventchar_select +schema_cannedmessage_inputeventchar_up +schema_cannedmessage_rotary1_enabled +schema_cannedmessage_rotary1_enabled_description +schema_cannedmessage_send_bell +schema_cannedmessage_send_bell_description +schema_cannedmessage_updown1_enabled +schema_cannedmessage_updown1_enabled_description +schema_detectionsensor_detection_trigger_type +schema_detectionsensor_detection_trigger_type_description +schema_detectionsensor_enabled +schema_detectionsensor_enabled_description +schema_detectionsensor_minimum_broadcast_secs +schema_detectionsensor_minimum_broadcast_secs_description +schema_detectionsensor_monitor_pin +schema_detectionsensor_monitor_pin_description +schema_detectionsensor_name +schema_detectionsensor_name_description +schema_detectionsensor_send_bell +schema_detectionsensor_send_bell_description +schema_detectionsensor_state_broadcast_secs +schema_detectionsensor_state_broadcast_secs_description +schema_detectionsensor_triggertype_either_edge_active_high +schema_detectionsensor_triggertype_either_edge_active_low +schema_detectionsensor_triggertype_falling_edge +schema_detectionsensor_triggertype_logic_high +schema_detectionsensor_triggertype_logic_low +schema_detectionsensor_triggertype_rising_edge +schema_detectionsensor_use_pullup +schema_detectionsensor_use_pullup_description +schema_device_button_gpio +schema_device_button_gpio_description +schema_device_buzzer_gpio +schema_device_buzzer_gpio_description +schema_device_disable_triple_click +schema_device_disable_triple_click_description +schema_device_double_tap_as_button_press +schema_device_double_tap_as_button_press_description +schema_device_led_heartbeat_disabled +schema_device_led_heartbeat_disabled_description +schema_device_node_info_broadcast_secs +schema_device_node_info_broadcast_secs_description +schema_device_rebroadcast_mode +schema_device_rebroadcastmode_all +schema_device_rebroadcastmode_all_description +schema_device_rebroadcastmode_all_skip_decoding +schema_device_rebroadcastmode_all_skip_decoding_description +schema_device_rebroadcastmode_core_portnums_only +schema_device_rebroadcastmode_core_portnums_only_description +schema_device_rebroadcastmode_known_only +schema_device_rebroadcastmode_known_only_description +schema_device_rebroadcastmode_local_only +schema_device_rebroadcastmode_local_only_description +schema_device_rebroadcastmode_none +schema_device_rebroadcastmode_none_description +schema_device_role +schema_device_role_client +schema_device_role_client_base +schema_device_role_client_base_description +schema_device_role_client_description +schema_device_role_client_hidden +schema_device_role_client_hidden_description +schema_device_role_client_mute +schema_device_role_client_mute_description +schema_device_role_lost_and_found +schema_device_role_lost_and_found_description +schema_device_role_repeater +schema_device_role_repeater_description +schema_device_role_router +schema_device_role_router_client +schema_device_role_router_description +schema_device_role_router_late +schema_device_role_router_late_description +schema_device_role_sensor +schema_device_role_sensor_description +schema_device_role_tak +schema_device_role_tak_description +schema_device_role_tak_tracker +schema_device_role_tak_tracker_description +schema_device_role_tracker +schema_device_role_tracker_description +schema_device_tzdef +schema_device_tzdef_description +schema_display_auto_screen_carousel_secs +schema_display_auto_screen_carousel_secs_description +schema_display_compass_north_top +schema_display_compass_north_top_description +schema_display_compass_orientation +schema_display_compass_orientation_description +schema_display_compassorientation_degrees_0 +schema_display_compassorientation_degrees_0_inverted +schema_display_compassorientation_degrees_180 +schema_display_compassorientation_degrees_180_inverted +schema_display_compassorientation_degrees_270 +schema_display_compassorientation_degrees_270_inverted +schema_display_compassorientation_degrees_90 +schema_display_compassorientation_degrees_90_inverted +schema_display_displaymode +schema_display_displaymode_color +schema_display_displaymode_default +schema_display_displaymode_description +schema_display_displaymode_inverted +schema_display_displaymode_twocolor +schema_display_displayunits_imperial +schema_display_displayunits_metric +schema_display_flip_screen +schema_display_flip_screen_description +schema_display_heading_bold +schema_display_heading_bold_description +schema_display_oled +schema_display_oled_description +schema_display_oledtype_oled_auto +schema_display_oledtype_oled_sh1106 +schema_display_oledtype_oled_sh1107 +schema_display_oledtype_oled_sh1107_128_128 +schema_display_oledtype_oled_sh1107_rotated +schema_display_oledtype_oled_ssd1306 +schema_display_screen_on_secs +schema_display_screen_on_secs_description +schema_display_units +schema_display_units_description +schema_display_use_12h_clock +schema_display_use_12h_clock_description +schema_display_wake_on_tap_or_motion +schema_display_wake_on_tap_or_motion_description +schema_externalnotification_active +schema_externalnotification_active_description +schema_externalnotification_alert_bell +schema_externalnotification_alert_bell_buzzer +schema_externalnotification_alert_bell_buzzer_description +schema_externalnotification_alert_bell_description +schema_externalnotification_alert_bell_vibra +schema_externalnotification_alert_bell_vibra_description +schema_externalnotification_alert_message +schema_externalnotification_alert_message_buzzer +schema_externalnotification_alert_message_buzzer_description +schema_externalnotification_alert_message_description +schema_externalnotification_alert_message_vibra +schema_externalnotification_alert_message_vibra_description +schema_externalnotification_enabled +schema_externalnotification_enabled_description +schema_externalnotification_nag_timeout +schema_externalnotification_nag_timeout_description +schema_externalnotification_output +schema_externalnotification_output_buzzer +schema_externalnotification_output_buzzer_description +schema_externalnotification_output_description +schema_externalnotification_output_ms +schema_externalnotification_output_ms_description +schema_externalnotification_output_vibra +schema_externalnotification_output_vibra_description +schema_externalnotification_use_i2s_as_buzzer +schema_externalnotification_use_i2s_as_buzzer_description +schema_externalnotification_use_pwm +schema_externalnotification_use_pwm_description +schema_lora_bandwidth +schema_lora_channel_num +schema_lora_channel_num_description +schema_lora_coding_rate +schema_lora_coding_rate_description +schema_lora_config_ok_to_mqtt +schema_lora_hop_limit +schema_lora_hop_limit_description +schema_lora_ignore_incoming +schema_lora_ignore_mqtt +schema_lora_ignore_mqtt_description +schema_lora_modem_preset +schema_lora_modempreset_lite_fast +schema_lora_modempreset_lite_slow +schema_lora_modempreset_long_fast +schema_lora_modempreset_long_moderate +schema_lora_modempreset_long_slow +schema_lora_modempreset_long_turbo +schema_lora_modempreset_medium_fast +schema_lora_modempreset_medium_slow +schema_lora_modempreset_medium_turbo +schema_lora_modempreset_narrow_fast +schema_lora_modempreset_narrow_slow +schema_lora_modempreset_short_fast +schema_lora_modempreset_short_slow +schema_lora_modempreset_short_turbo +schema_lora_modempreset_tiny_fast +schema_lora_modempreset_tiny_slow +schema_lora_modempreset_very_long_slow +schema_lora_override_duty_cycle +schema_lora_override_frequency +schema_lora_pa_fan_disabled +schema_lora_region +schema_lora_region_description +schema_lora_regioncode_anz +schema_lora_regioncode_anz_433 +schema_lora_regioncode_br_902 +schema_lora_regioncode_cn +schema_lora_regioncode_eu_433 +schema_lora_regioncode_eu_866 +schema_lora_regioncode_eu_868 +schema_lora_regioncode_eu_874 +schema_lora_regioncode_eu_917 +schema_lora_regioncode_eu_n_868 +schema_lora_regioncode_in +schema_lora_regioncode_itu1_2m +schema_lora_regioncode_itu1_70cm +schema_lora_regioncode_itu2_125cm +schema_lora_regioncode_itu2_2m +schema_lora_regioncode_itu2_70cm +schema_lora_regioncode_itu3_2m +schema_lora_regioncode_itu3_70cm +schema_lora_regioncode_jp +schema_lora_regioncode_kr +schema_lora_regioncode_kz_433 +schema_lora_regioncode_kz_863 +schema_lora_regioncode_lora_24 +schema_lora_regioncode_my_433 +schema_lora_regioncode_my_919 +schema_lora_regioncode_np_865 +schema_lora_regioncode_nz_865 +schema_lora_regioncode_ph_433 +schema_lora_regioncode_ph_868 +schema_lora_regioncode_ph_915 +schema_lora_regioncode_ru +schema_lora_regioncode_sg_923 +schema_lora_regioncode_th +schema_lora_regioncode_tw +schema_lora_regioncode_ua_433 +schema_lora_regioncode_ua_868 +schema_lora_regioncode_unset +schema_lora_regioncode_us +schema_lora_spread_factor +schema_lora_spread_factor_description +schema_lora_sx126x_rx_boosted_gain +schema_lora_sx126x_rx_boosted_gain_description +schema_lora_tx_enabled +schema_lora_tx_enabled_description +schema_lora_tx_power +schema_lora_tx_power_description +schema_lora_use_preset +schema_lora_use_preset_description +schema_mapreportsettings_publish_interval_secs +schema_mapreportsettings_publish_interval_secs_description +schema_mapreportsettings_should_report_location +schema_mapreportsettings_should_report_location_description +schema_memberrole_forwardobserver +schema_memberrole_hq +schema_memberrole_k9 +schema_memberrole_medic +schema_memberrole_rto +schema_memberrole_sniper +schema_memberrole_teamlead +schema_memberrole_teammember +schema_memberrole_unspecifed +schema_meshbeacon_broadcast_interval_secs +schema_meshbeacon_broadcast_interval_secs_description +schema_meshbeacon_broadcast_message +schema_meshbeacon_broadcast_message_description +schema_mqtt_address +schema_mqtt_address_description +schema_mqtt_enabled +schema_mqtt_enabled_description +schema_mqtt_encryption_enabled +schema_mqtt_encryption_enabled_description +schema_mqtt_map_reporting_enabled +schema_mqtt_map_reporting_enabled_description +schema_mqtt_password +schema_mqtt_password_description +schema_mqtt_proxy_to_client_enabled +schema_mqtt_proxy_to_client_enabled_description +schema_mqtt_root +schema_mqtt_root_description +schema_mqtt_tls_enabled +schema_mqtt_tls_enabled_description +schema_mqtt_username +schema_mqtt_username_description +schema_neighborinfo_enabled +schema_neighborinfo_enabled_description +schema_neighborinfo_transmit_over_lora +schema_neighborinfo_transmit_over_lora_description +schema_neighborinfo_update_interval +schema_neighborinfo_update_interval_description +schema_network_address_mode +schema_network_addressmode_dhcp +schema_network_addressmode_static +schema_network_enabled_protocols +schema_network_enabled_protocols_description +schema_network_eth_enabled +schema_network_eth_enabled_description +schema_network_ipv4_dns +schema_network_ipv4_gateway +schema_network_ipv4_ip +schema_network_ipv4_subnet +schema_network_ntp_server +schema_network_ntp_server_description +schema_network_protocolflags_no_broadcast +schema_network_protocolflags_udp_broadcast +schema_network_rsyslog_server +schema_network_wifi_enabled +schema_network_wifi_enabled_description +schema_network_wifi_psk +schema_network_wifi_psk_description +schema_network_wifi_ssid +schema_network_wifi_ssid_description +schema_paxcounter_ble_threshold +schema_paxcounter_ble_threshold_description +schema_paxcounter_enabled +schema_paxcounter_enabled_description +schema_paxcounter_paxcounter_update_interval +schema_paxcounter_paxcounter_update_interval_description +schema_paxcounter_wifi_threshold +schema_paxcounter_wifi_threshold_description +schema_position_broadcast_smart_minimum_distance +schema_position_broadcast_smart_minimum_distance_description +schema_position_broadcast_smart_minimum_interval_secs +schema_position_broadcast_smart_minimum_interval_secs_description +schema_position_fixed_position +schema_position_fixed_position_description +schema_position_gps_en_gpio +schema_position_gps_en_gpio_description +schema_position_gps_mode +schema_position_gps_update_interval +schema_position_gps_update_interval_description +schema_position_gpsmode_disabled +schema_position_gpsmode_enabled +schema_position_gpsmode_not_present +schema_position_position_broadcast_secs +schema_position_position_broadcast_secs_description +schema_position_position_broadcast_smart_enabled +schema_position_position_flags +schema_position_position_flags_description +schema_position_positionflags_altitude +schema_position_positionflags_altitude_description +schema_position_positionflags_altitude_msl +schema_position_positionflags_dop +schema_position_positionflags_dop_description +schema_position_positionflags_geoidal_separation +schema_position_positionflags_heading +schema_position_positionflags_hvdop +schema_position_positionflags_hvdop_description +schema_position_positionflags_satinview +schema_position_positionflags_seq_no +schema_position_positionflags_speed +schema_position_positionflags_timestamp +schema_position_rx_gpio +schema_position_rx_gpio_description +schema_position_tx_gpio +schema_position_tx_gpio_description +schema_power_adc_multiplier_override +schema_power_is_power_saving +schema_power_is_power_saving_description +schema_power_on_battery_shutdown_after_secs +schema_power_on_battery_shutdown_after_secs_description +schema_power_wait_bluetooth_secs +schema_rangetest_enabled +schema_rangetest_enabled_description +schema_rangetest_save +schema_rangetest_save_description +schema_rangetest_sender +schema_rangetest_sender_description +schema_security_admin_key +schema_security_admin_key_description +schema_security_debug_log_api_enabled +schema_security_debug_log_api_enabled_description +schema_security_is_managed +schema_security_is_managed_description +schema_security_packetsignaturepolicy_packet_signature_policy_balanced +schema_security_packetsignaturepolicy_packet_signature_policy_balanced_description +schema_security_packetsignaturepolicy_packet_signature_policy_compatible +schema_security_packetsignaturepolicy_packet_signature_policy_compatible_description +schema_security_packetsignaturepolicy_packet_signature_policy_strict +schema_security_packetsignaturepolicy_packet_signature_policy_strict_description +schema_security_private_key +schema_security_private_key_description +schema_security_public_key +schema_security_public_key_description +schema_security_serial_enabled +schema_security_serial_enabled_description +schema_serial_baud +schema_serial_baud_description +schema_serial_echo +schema_serial_echo_description +schema_serial_enabled +schema_serial_enabled_description +schema_serial_mode +schema_serial_mode_description +schema_serial_rxd +schema_serial_rxd_description +schema_serial_serial_baud_baud_110 +schema_serial_serial_baud_baud_115200 +schema_serial_serial_baud_baud_1200 +schema_serial_serial_baud_baud_19200 +schema_serial_serial_baud_baud_230400 +schema_serial_serial_baud_baud_2400 +schema_serial_serial_baud_baud_300 +schema_serial_serial_baud_baud_38400 +schema_serial_serial_baud_baud_460800 +schema_serial_serial_baud_baud_4800 +schema_serial_serial_baud_baud_57600 +schema_serial_serial_baud_baud_576000 +schema_serial_serial_baud_baud_600 +schema_serial_serial_baud_baud_921600 +schema_serial_serial_baud_baud_9600 +schema_serial_serial_baud_baud_default +schema_serial_serial_mode_caltopo +schema_serial_serial_mode_default +schema_serial_serial_mode_nmea +schema_serial_serial_mode_proto +schema_serial_serial_mode_simple +schema_serial_serial_mode_textmsg +schema_serial_timeout +schema_serial_timeout_description +schema_serial_txd +schema_serial_txd_description +schema_storeforward_enabled +schema_storeforward_enabled_description +schema_storeforward_heartbeat +schema_storeforward_heartbeat_description +schema_storeforward_history_return_max +schema_storeforward_history_return_window +schema_storeforward_is_server +schema_storeforward_is_server_description +schema_storeforward_records +schema_tak_role +schema_tak_role_description +schema_tak_team +schema_tak_team_description +schema_team_blue +schema_team_brown +schema_team_cyan +schema_team_dark_blue +schema_team_dark_green +schema_team_green +schema_team_magenta +schema_team_maroon +schema_team_orange +schema_team_purple +schema_team_red +schema_team_teal +schema_team_unspecifed_color +schema_team_white +schema_team_yellow +schema_telemetry_air_quality_enabled +schema_telemetry_air_quality_enabled_description +schema_telemetry_air_quality_interval +schema_telemetry_air_quality_interval_description +schema_telemetry_device_telemetry_enabled +schema_telemetry_device_telemetry_enabled_description +schema_telemetry_device_update_interval +schema_telemetry_device_update_interval_description +schema_telemetry_environment_display_fahrenheit +schema_telemetry_environment_display_fahrenheit_description +schema_telemetry_environment_measurement_enabled +schema_telemetry_environment_measurement_enabled_description +schema_telemetry_environment_screen_enabled +schema_telemetry_environment_screen_enabled_description +schema_telemetry_environment_update_interval +schema_telemetry_environment_update_interval_description +schema_telemetry_power_measurement_enabled +schema_telemetry_power_measurement_enabled_description +schema_telemetry_power_screen_enabled +schema_telemetry_power_screen_enabled_description +schema_telemetry_power_update_interval +schema_telemetry_power_update_interval_description +schema_trafficmanagement_nodeinfo_direct_response_max_hops +schema_trafficmanagement_nodeinfo_direct_response_max_hops_description +schema_trafficmanagement_position_min_interval_secs +schema_trafficmanagement_position_min_interval_secs_description +schema_trafficmanagement_rate_limit_max_packets +schema_trafficmanagement_rate_limit_max_packets_description +schema_trafficmanagement_rate_limit_window_secs +schema_trafficmanagement_rate_limit_window_secs_description +schema_trafficmanagement_unknown_packet_threshold +schema_trafficmanagement_unknown_packet_threshold_description diff --git a/.skills/design-standards/SKILL.md b/.skills/design-standards/SKILL.md index 61f4492841..45cd6e4bc4 100644 --- a/.skills/design-standards/SKILL.md +++ b/.skills/design-standards/SKILL.md @@ -1,3 +1,8 @@ +--- +name: design-standards +description: Apply the Meshtastic design standards on Android - brand colours, Material 3 tokens, MeshtasticIcons and accessibility - with the upstream `meshtastic/design` standards as the source of truth. Use this whenever you choose a colour, icon, spacing value or contrast level, and before claiming a screen matches the design. +--- + # Skill: Meshtastic Design Standards ## Description diff --git a/.skills/implement-feature/SKILL.md b/.skills/implement-feature/SKILL.md index 5f0a327696..d1a647bbfa 100644 --- a/.skills/implement-feature/SKILL.md +++ b/.skills/implement-feature/SKILL.md @@ -1,3 +1,8 @@ +--- +name: implement-feature +description: The end-to-end workflow for adding a feature to Meshtastic-Android, in the order that keeps KMP source sets and the architecture intact. Use this whenever you start implementing a new feature or a sizeable behaviour change, before writing any code. +--- + # Skill: Implement a Feature ## Description @@ -33,7 +38,7 @@ A step-by-step workflow for implementing a new feature in the Meshtastic-Android ### 6. Verify Locally - Run the baseline checks (see `testing-ci` skill): ```bash - ./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests + ./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests ``` - Add `kmpSmokeCompile` to the command when the feature touches a KMP module (most features do) so cross-target compile failures surface before merge. - If the feature adds a new reflection-heavy dependency, add keep rules to **both** `androidApp/proguard-rules.pro` and `desktopApp/proguard-rules.pro`, then verify release builds: diff --git a/.skills/kmp-architecture/SKILL.md b/.skills/kmp-architecture/SKILL.md index 8858f3cc93..8e80de4109 100644 --- a/.skills/kmp-architecture/SKILL.md +++ b/.skills/kmp-architecture/SKILL.md @@ -1,3 +1,8 @@ +--- +name: kmp-architecture +description: Kotlin Multiplatform source-set rules for Meshtastic-Android - where commonMain ends, how expect/actual is bridged, and the networking, database and platform-integration boundaries. Use this whenever you add a file to a KMP module, move code between source sets, or hit a compile error that appears on only one target. +--- + # Skill: KMP Architecture & Source-Set Bridging ## Description @@ -6,7 +11,8 @@ Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstract ## 1. Source-Set Boundaries - **`commonMain`:** All business logic, DB entities, API network logic, ViewModels, and UI rendering. NO `java.*` or `android.*` imports. - **`androidMain`:** Android framework integration (`Context`, system services, NFC hardware, BLE Android bindings). -- **`jvmMain` / `jvmAndroidMain`:** Shared JVM code between Android and Desktop. Uses the `meshtastic.kmp.jvm.android` convention plugin to bridge `jvm` and `android` source sets without manual `dependsOn` hacks. +- **`jvmMain`:** Desktop-only JVM code. +- **`jvmAndroidMain`:** JVM code shared between Android and Desktop. Uses the `meshtastic.kmp.jvm.android` convention plugin to bridge `jvm` and `android` source sets without manual `dependsOn` hacks. - **`androidApp` / `desktopApp`:** Host shells. Responsible for Koin DI root wiring (`MainKoinModule`/`AppKoinModule`, `DesktopKoinModule`), host-level UI themes, and running the `MeshtasticNavDisplay`. ## 2. Bridging Strategies @@ -49,7 +55,7 @@ Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstract - In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative. ## 8. Onboarding a New Target (Desktop/iOS) -1. Ensure all new logic compiles against the KMP core (`jvm()`, `iosArm64()`, etc.). +1. Ensure all new logic compiles against the KMP core targets (`jvm()`, `iosSimulatorArm64()`). 2. Do not use platform-specific constructs in `commonMain` or you break the iOS/Desktop builds. 3. Test using `kmpSmokeCompile` to verify cross-platform compilation. 4. For desktop wiring, copy the pattern in `desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations. diff --git a/.skills/navigation-and-di/SKILL.md b/.skills/navigation-and-di/SKILL.md index d4ac220849..68f4d7e4dd 100644 --- a/.skills/navigation-and-di/SKILL.md +++ b/.skills/navigation-and-di/SKILL.md @@ -1,7 +1,12 @@ +--- +name: navigation-and-di +description: Koin Annotations dependency injection and JetBrains Navigation 3 in Meshtastic-Android, including the anti-patterns that compile cleanly and then fail at runtime. Use this whenever you add a screen, a route, a ViewModel or a Koin module, or when navigation or injection behaves unexpectedly. +--- + # Skill: DI and Navigation 3 Architecture ## Description -This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Navigation 3 (1.1.x) architecture, constraints, and anti-patterns within the Meshtastic-Android KMP codebase. +This skill covers dependency injection (Koin Annotations 4.2.x) and Navigation 3 1.2 (the JetBrains `navigation3-ui` mirror over AndroidX `navigation3-runtime`) architecture, constraints, and anti-patterns within the Meshtastic-Android KMP codebase. ## Dependency Injection (Koin) @@ -41,14 +46,17 @@ startKoin { 1. **Types:** Use Navigation 3 types consistently (`NavKey`, `NavBackStack`, `EntryProviderScope`). 2. **Typed Routes:** Keep route definitions in `core:navigation/src/commonMain/.../Routes.kt` as `@Serializable sealed interface` hierarchies. Don't use ad-hoc strings. 3. **Graph Assembly:** Define feature navigation graphs as extension functions on `EntryProviderScope` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(backStack)`). -4. **Host Integration:** Use `MeshtasticNavDisplay` (from `core:ui/commonMain`) as the Navigation 3 host. Do not configure decorators manually inside feature modules. -5. **Back Handlers:** Use `NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose` for back gestures in multiplatform code. Do not use Android's `BackHandler`. -6. **Deep Links:** Use `DeepLinkRouter.route()` in `core:navigation` to synthesize typed backstacks from RESTful paths. +4. **Host Integration:** Use `MeshtasticNavDisplay` (from `core:ui/commonMain`) as the Navigation 3 host. It owns the entry decorators; do not create them in app hosts or feature modules. +5. **Scenes:** `MeshtasticNavDisplay` renders `ListDetailSceneStrategy` scenes (`listPane()`, `detailPane()`, `extraPane()` entry metadata) and falls back to a single pane. It registers no dialog or supporting-pane strategy, so that metadata has no effect. +6. **Back Handlers:** Use `NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose` for back gestures in multiplatform code. Do not use Android's `BackHandler`. +7. **Deep Links:** Use `DeepLinkRouter.route()` in `core:navigation` to synthesize typed backstacks from RESTful paths. +8. **Tab Lifetime:** A hidden tab's entry ViewModels and saved state live until that entry is popped from its own stack; switching tabs does not clear them. ### Anti-Patterns - **Single Backstack for Multiple Tabs:** Do **not** use a single `NavBackStack` list for multiple tabs. Use `MultiBackstack` (from `core:navigation`). -- **Decorator Reuse Across Tabs:** Do **not** reuse the same `NavEntryDecorator` instances across different backstacks. When rendering an active tab in `MeshtasticNavDisplay`, you **must** supply a fresh set of decorators (using `remember(backStack) { ... }`) bound to the active backstack instance to prevent permanent `ViewModelStore` destruction. +- **Decorator Reuse Across Tabs:** Do **not** decorate several back stacks with one `NavEntryDecorator` set. Navigation 3 pops every entry missing from the stack it is given, so a shared saveable-state or ViewModel-store decorator clears the tab you just left. The `MultiBackstack` overload of `MeshtasticNavDisplay` gives every tab's stack its own saveable-state and ViewModel-store decorators through `rememberDecoratedNavEntries`, following the per-stack decorators of the Navigation 3 multiple back stacks recipe, and passes only the active tab's entries to `NavDisplay`. Its `entryProvider` must therefore resolve every tab's keys, not only the active tab's. - **Custom Backstack Mutation:** Do **not** mutate back navigation with custom stacks disconnected from the app backstack. Mutate `NavBackStack` directly with `add(...)` and `removeLastOrNull()`. +- **Inline Entries in Nav Tests:** Do **not** write a test's entries inline in a composable host. `entryProvider` and `entry` are `inline`, so inline entries become remembered lambdas the compiler updates in place and a stale back-stack capture passes unseen. Declare them in a plain `EntryProviderScope` extension, as feature graphs do. ## Reference Anchors - **App Startup / Koin Bootstrap:** `androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` diff --git a/.skills/new-branch/SKILL.md b/.skills/new-branch/SKILL.md index faa0da8546..df59095c4b 100644 --- a/.skills/new-branch/SKILL.md +++ b/.skills/new-branch/SKILL.md @@ -1,3 +1,8 @@ +--- +name: new-branch +description: The canonical recipe for starting a fresh working branch off a freshly fetched `origin/main` in Meshtastic-Android. Use this whenever a unit of work starts - "new branch off main", "peel off a fresh branch", "dust off #NNNN" - so the branch is based correctly and nothing is carried over from the last one. +--- + # Skill: New Branch Bootstrap ## Description @@ -66,6 +71,7 @@ When the user says *"rebase #NNNN"* or *"dust off PR NNNN"*: ```bash git fetch upstream --prune gh pr checkout # checks out the PR head locally +git branch --show-current # confirm this is the PR you meant, before any push git rebase upstream/main # Resolve conflicts, then: git push --force-with-lease @@ -73,6 +79,11 @@ git push --force-with-lease Never use plain `--force`. Always `--force-with-lease` to avoid clobbering collaborator pushes. +Read that branch name before you push. A mistyped `` is a valid PR number +belonging to someone else, and `--force-with-lease` will not save you: it only +refuses when the remote ref moved after you fetched it, which is exactly not the +case here. The lease is intact and the wrong branch gets rewritten. + ## Post-Branch Checklist - [ ] Branch name carries a conventional prefix, or is a spec-driven name (numeric or `YYYYMMDD-HHMMSS-`) per Branch Naming above. - [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap). diff --git a/.skills/project-overview/SKILL.md b/.skills/project-overview/SKILL.md index fb67eb908d..3daf4e7812 100644 --- a/.skills/project-overview/SKILL.md +++ b/.skills/project-overview/SKILL.md @@ -1,11 +1,16 @@ +--- +name: project-overview +description: The Meshtastic-Android codebase map - module directory, namespacing conventions, JDK and SDK requirements, and the mandatory bootstrap steps. Read this first in any session that will build, test or navigate the repo: the bootstrap here has to run before any Gradle task, and skipping it is what makes the first build fail. +--- + # Skill: Project Overview & Codebase Map ## Description Module directory, namespacing conventions, environment setup, and troubleshooting for Meshtastic-Android. -- **Build System:** Gradle (Kotlin DSL). JDK 25 REQUIRED. Target SDK: API 36. Min SDK: API 26. +- **Build System:** Gradle (Kotlin DSL). JDK 25 REQUIRED. Target SDK: API 37. Min SDK: API 26. - **Flavors:** `fdroid` (OSS only) · `google` (Maps + DataDog analytics) -- **Android-only Modules:** `core:barcode` (CameraX), `feature:widget` (Glance home-screen widget), and `baselineprofile` (Macrobenchmark). Shared contracts are abstracted into `core:ui/commonMain`. +- **Android-only Modules:** `core:barcode` (CameraX), `core:nfc` (NFC hardware), `feature:widget` (Glance home-screen widget), and `baselineprofile` (Macrobenchmark). Shared contracts are abstracted into `core:ui/commonMain`. ## Codebase Map @@ -22,7 +27,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). | | `core:domain` | Pure KMP business logic and UseCases. | | `core:data` | Core manager implementations and data orchestration. | -| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). | +| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioTransport`). | | `core:di` | Common DI qualifiers and dispatchers. | | `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies. `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence. | | `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. | @@ -30,11 +35,11 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | `core:takserver` | Meshtastic ↔ TAK (ATAK/iTAK) bridge — local CoT server and CoT ⇄ mesh conversion. | | `core:prefs` | KMP preferences layer built on DataStore abstractions. | | `core:barcode` | Barcode scanning (Android-only). | -| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. | +| `core:nfc` | Android-only NFC hardware implementation (an Android library, not KMP). The shared NFC abstractions are the `LocalNfc*Provider` composition locals in `core:ui`. | | `core/ble/` | Bluetooth Low Energy stack using Kable. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules. | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `discovery`, `docs`, `widget`). Most are KMP and use the `meshtastic.kmp.feature` convention plugin; `widget` (Glance) is Android-only. | +| `feature/` | Feature modules (e.g., `settings`, `map`, `map-maplibre`, `map-terrain`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `discovery`, `docs`, `widget`). Most are KMP and use the `meshtastic.kmp.feature` convention plugin; `widget` (Glance) is Android-only. | | `baselineprofile/` | Macrobenchmark Baseline Profile generation for `:androidApp` (AOT-compiled cold-start journey). Android-only. | | `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. | | `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. | diff --git a/.skills/speckit/SKILL.md b/.skills/speckit/SKILL.md index 2b4d091764..54fb8a3020 100644 --- a/.skills/speckit/SKILL.md +++ b/.skills/speckit/SKILL.md @@ -1,3 +1,8 @@ +--- +name: speckit +description: The Spec Kit specification-driven workflow in Meshtastic-Android - how a feature description becomes a spec, a plan, tasks and an implementation, and what the constitution requires at each step. Use this when asked to write or update a spec, plan or tasks file, or when working anywhere under `specs/`. +--- + # Skill: Spec Kit (Specification-Driven Development) ## Description @@ -108,14 +113,14 @@ specs/ The project constitution at `.specify/memory/constitution.md` defines non-negotiable principles. All specs, plans, and tasks are validated against it during `/speckit.analyze`. -Current constitution (v1.4.0) enforces 7 principles: +Current constitution (v1.4.2) enforces 7 principles: 1. **KMP Core** — Business logic in `commonMain` only 2. **Zero Lint Tolerance** — `spotlessCheck` + `detekt` must pass 3. **Compose Multiplatform UI** — CMP, not Android-only Compose 4. **Privacy First** — No PII/location/key exposure 5. **Design Standards Compliance** — Review against Meshtastic design standards; cross-platform features must reference an upstream spec from `meshtastic/design/features/` -6. **Documentation Freshness** — User-facing changes update `docs/en/` (in-app browser, Jekyll, Docusaurus) with `last_updated` frontmatter; links, coverage, and the DocBundleLoader registry are a blocking CI gate (`docs-quality.yml`) on PRs touching `docs/en/**`, freshness advisory +6. **Documentation Freshness** — User-facing changes update `docs/en/` (in-app browser, Jekyll, Docusaurus) with `last_updated` frontmatter; links, coverage, the DocBundleLoader registry, and locale `layout`/`nav_order` matching `docs/en/` with no locale `parent` are a blocking CI gate (`docs-quality.yml`) on PRs touching `docs/**/*.md`, freshness advisory 7. **Verify Before Push** — Local verification before any `git push` ## Extension Hooks diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md index 6a71f078eb..26b8a87bb9 100644 --- a/.skills/testing-ci/SKILL.md +++ b/.skills/testing-ci/SKILL.md @@ -1,3 +1,8 @@ +--- +name: testing-ci +description: Decide what to run locally before pushing Meshtastic-Android, and read the CI pipeline. Use this whenever you finish a change and need the right verification for its change type, when a CI job fails or is skipped, or when you need to know why the merge queue behaved the way it did. +--- + # Skill: Testing and CI Verification ## Description @@ -8,9 +13,13 @@ Guidelines and commands for verifying code changes locally and understanding the Run in a single invocation for routine changes to ensure code formatting, analysis, and basic compilation: ```bash -./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests +./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests ``` +`detekt` alone has no classpath, so it skips every rule that needs type resolution (`UnsafeCallOnNullableType`, `SuspendFunSwallowedCancellation`, `UnsafeCast` and the rest). `detektTypeResolved` runs those against each module's production JVM and Android debug compilations; it shares the module's `detekt-baseline.xml`. + +Regenerating that baseline needs care, because detekt's baseline tasks rewrite `CurrentIssues` from their own run and keep only `ManuallySuppressedIssues`. A plain `detektBaseline` therefore drops every type-resolved ID, and `detektBaselineMain` or `detektBaseline` writes `detekt-baseline-.xml`, which no check reads. After a plain `detektBaseline`, run the type-resolved baseline tasks for that module (`detektBaselineMainJvm`, `detektBaselineMainAndroid`, `detektBaselineFdroidDebug` and so on), copy the IDs you mean to keep from the generated files into `detekt-baseline.xml`, delete the generated files, and confirm with `detekt detektTypeResolved`. `detektBaselineMainJvm` and `detektBaselineMainAndroid` both write `detekt-baseline-main.xml`, so run and copy them one at a time. + > **Why no `clean`?** Incremental builds are safe and significantly faster. Only use `clean` when debugging stale cache issues. > **Why `test allTests` and not just `test`:** @@ -52,6 +61,7 @@ When testing long-lived coroutines (e.g., `Flow.collect` loops launched in `back - If touching any KMP module, also run `kmpSmokeCompile`. - `worker/service/background` changes: Broad tests, targeted WorkManager checks. - `BLE/networking/core repository`: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`. +- `build-logic/` changes: also `:build-logic:convention:spotlessCheck :build-logic:convention:detekt`, which the root `spotlessCheck detekt` do not reach. ## 3) Flavor checks @@ -79,65 +89,70 @@ Rendering is **host-deterministic** (layoutlib): a local `update` produces refer ## 3c) Fresh-install manual/agent testing: skip onboarding -Debug builds accept an intent extra to skip the intro flow (`MainActivity.kt`, `BuildConfig.DEBUG`-gated — never reaches release/Play builds). Pair with `pm grant` (native Android, no app code) to pre-accept runtime permissions: +Debug builds accept an intent extra to skip the intro flow, honoured only on a launch through the `AutomationLauncher` alias (`androidApp/src/debug/AndroidManifest.xml`), which requires `DUMP` so only the shell can start it. Release builds have no alias. Pair with `pm grant` (native Android, no app code) to pre-accept runtime permissions: ```bash adb shell pm grant android.permission.BLUETOOTH_SCAN adb shell pm grant android.permission.BLUETOOTH_CONNECT adb shell pm grant android.permission.ACCESS_FINE_LOCATION adb shell pm grant android.permission.POST_NOTIFICATIONS # API 33+ -adb shell am start -n /org.meshtastic.app.MainActivity --ez skip_onboarding true +adb shell am start -n /org.meshtastic.app.AutomationLauncher --ez skip_onboarding true ``` Use this whenever driving the app from a fresh install/uninstall (screenshot tests, UI automation, agent-driven exploration) instead of clicking through the intro screens. ## 4) CI Pipeline Architecture -CI is defined in `.github/workflows/reusable-check.yml` and structured as parallel job groups: +CI is defined in `.github/workflows/reusable-check.yml` as parallel job groups. No job `needs:` another, so every one queues for a runner as soon as the run starts: -1. **`lint-check`** — Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation (avoids 3x cold-start overhead). Uses `fetch-depth: 0` (full clone) for spotless ratcheting and version code calculation. Produces `cache_read_only` output and computed `version_code` for downstream jobs. -2. **`test-shards`** — A 3-shard matrix that runs unit tests in parallel (depends on `lint-check`). Shard membership is a load-balancing detail, not a taxonomy — heavy modules are moved between shards to even out wall time, so read the matrix rather than inferring it: - - `shard-core`: `allTests` for the remaining `core:*` KMP modules. +1. **`lint-check`** runs `spotlessCheck`, `detekt`, `detektTypeResolved` and Android lint for both flavors of `:androidApp` and `:core:barcode` in a single Gradle invocation (avoids 3x cold-start overhead), plus `:build-logic:convention:spotlessCheck` and `:build-logic:convention:detekt`, because the root tasks do not reach the included build. It checks out full history (`fetch-depth: 0`) because spotless ratchets against `origin/main`, and it has no outputs. +2. **`test-shards`** is a 3-shard matrix that runs unit tests in parallel. Shard membership is a load-balancing detail, not a taxonomy: heavy modules are moved between shards to even out wall time, so read the matrix rather than inferring it: + - `shard-core`: `allTests` for the remaining `core:*` KMP modules, plus `kmpSmokeCompile`. - `shard-feature`: `allTests` for `feature:*` KMP modules **plus `:core:service`**. - - `shard-app`: Explicit test tasks for pure-Android/JVM modules (`androidApp`, `desktopApp`, `core:barcode`). - Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags. - Downstream jobs use `fetch-depth: 1` and receive `VERSION_CODE` from lint-check via env var, enabling shallow clones. -3. **`android-check`** — Builds APKs for all flavors (depends on `lint-check`). -4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) running `:desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars` (depends on `lint-check`). It packages the debug build type — real installers the snapshot release can ship — and pulls in `proguardReleaseJars` only so a jmods-less packaging JDK fails here rather than at release time. On Linux it then wraps jpackage's `app-image` directory into a real AppImage via `scripts/build-appimage.sh`. + - `shard-app`: Explicit test tasks for pure-Android/JVM modules (`androidApp`, `desktopApp`, `core:barcode`, `feature:widget`, `schema-strings`) **plus `:core:database` and `:core:network`**. + Every shard uploads its test results to Codecov. Kover XML coverage is generated and uploaded only when `run_coverage` is true, which only `main-check.yml` passes. Codecov flags follow the module group (`core`, `feature`, `app`, `desktop`), not the shard. + The validation-only jobs (`lint-check`, `screenshot-check`, `test-shards`) pin `VERSION_CODE` to one constant so the versionCode-dependent tasks keep the same cache keys on every commit; `screenshot-check` and `test-shards` also clone shallow (`fetch-depth: 1`). `android-check` and `build-desktop` check out full blob-less history so the build derives the real versionCode. +3. **`android-check`** builds the fdroid and google debug APKs and checks their native-library ABI parity (`scripts/verify-abi-parity.sh`). The merge queue skips it. On `main` it also generates and submits the dependency graph; no other ref submits one. +4. **`build-desktop`** is a multi-OS matrix (macOS, Windows, and Linux x64 and arm64; the job's `matrix.os` carries the labels) running `:desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars`. It packages the debug build type, real installers the snapshot release can ship, and pulls in `proguardReleaseJars` only so a jmods-less packaging JDK fails here rather than at release time. On Linux it then wraps jpackage's `app-image` directory into a real AppImage via `scripts/build-appimage.sh`. 5. **`screenshot-check`** — Runs `:screenshot-tests:validateDebugScreenshotTest` (the visual-regression gate) and uploads a diff report. Note: `:docs-screenshots` is intentionally NOT validated here (generate-only). 6. **`rb-check`** — Reproducible-build verification (`scripts/verify-rb.sh`). Runs **only** in the merge queue. -7. **`verify-flatpak`** — Lives in its own workflow (`.github/workflows/verify-flatpak.yml`), **not** in `reusable-check.yml`, and is not called by it. Generates the Flatpak offline-build sources (`captureFlatpakSources`) and then builds the flatpak fully offline, on an `ubuntu-24.04` + `ubuntu-24.04-arm` matrix. Since #6919 the sources are generated inside each arch's own offline build rather than committed. It is **not a required check** and never runs in the merge queue, so its triggers are scoped accordingly: a PR runs it only when it touches the flatpak tooling itself (`scripts/verify-flatpak/**`, the workflow), while the wider dependency surface it captures (`gradle/libs.versions.toml`, `desktopApp/**`, `gradle/wrapper/**`, the root build scripts) is verified on push to `main` plus a nightly cron. +7. **`verify-flatpak`** lives in its own workflow (`.github/workflows/verify-flatpak.yml`), **not** in `reusable-check.yml`, and is not called by it. Generates the Flatpak offline-build sources (`captureFlatpakSources`) and then builds the flatpak fully offline, on an x86_64 + aarch64 matrix of hosted Ubuntu runners. Since #6919 the sources are generated inside each arch's own offline build rather than committed. It is **not a required check** and never runs in the merge queue, so its triggers are scoped accordingly: a PR runs it only when it touches the flatpak tooling itself (`scripts/verify-flatpak/**`, the workflow), and a push to `main` runs it for those plus `gradle/wrapper/**`, because the offline manifest pins the Gradle distribution apart from the wrapper. The wider dependency surface it captures (`desktopApp/**`, `gradle/libs.versions.toml`, the root build scripts) is verified by the nightly cron. + +8. **`protobufs-bump`** runs in its own workflow (`.github/workflows/protobufs-bump.yml`) on any PR that changes the `meshtastic-protobufs` line of `gradle/libs.versions.toml`. Comment only: it previews `:schema-strings:sync` at the new pin and leaves one sticky comment from `scripts/protobufs-bump-summary.py` with the upstream compare, the merged protobufs PRs, the `.proto` delta and the settings strings that will change. Nothing is pushed to the branch. The regeneration itself is a step of `scheduled-updates.yml`, which runs every 6 hours at :17 and on every push to `main` that changes `gradle/libs.versions.toml`. `values/schema_strings.xml` records the pin it was built from; each run compares that with the catalog and, when they differ, reuses the `scheduled-updates` branch's copy if it was already built for the catalog's pin from the same `schema-strings/` generator and `strings.xml` header, running Gradle only otherwise. The regenerated English goes up to Crowdin in the same run and rides the scheduled PR with the translations. `RepositorySyncTest` checks the file against the registry only while the recorded pin matches the catalog, so the window between a bump merging and the scheduled PR landing is not red. ### Runner Strategy (Four Tiers) -- **`ubuntu-slim`** — The cheapest tier, and where lightweight jobs belong since #6674/#6677: status gates, labelers, triage, run-cancellers, stale, changelog and release cleanup. Container-backed and starts in seconds, but **single-CPU, unprivileged, x64-only, with a hard 15-minute job cap** — so it fits `gh`/`jq`/`git`/stdlib-`python3`/`github-script` work and nothing needing `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. -- **`ubuntu-24.04-arm`** — Lightweight jobs that break any of those `ubuntu-slim` constraints (release metadata, `main-check`, promotion, dependency-graph submission). Shorter queue times than x64. -- **`ubuntu-24.04`** — Gradle-heavy jobs. Every single-runner job in `reusable-check.yml` pins it (`lint-check`, `screenshot-check`, `rb-check`, `test-shards`, `android-check`), as do release builds, Dokka and docs publishing. The `build-desktop` matrix job spans several runners instead, as does `verify-flatpak` in its own workflow. Pin for reproducibility. -- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job, `verify-flatpak`, and release packaging. +The tiers are named here and the workflows carry the label versions. + +- **`ubuntu-slim`** is the cheapest tier, for API and script jobs off the required-check path: the labeler, the PR-close run canceller, changelog, the docs link check, the Play listing upload, release cleanup, and the release and promotion jobs that are only API and script work (tag resolution, the version bump, the flatpak-sources and store-screenshot release assets, the GitHub release update, and the Homebrew and Flathub bumps). Container-backed and starts in seconds, but **single-CPU, unprivileged, x64-only, with a hard 15-minute job cap**, so it fits `gh`/`jq`/`git`/stdlib-`python3`/`github-script` work and nothing needing `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. It is a separate, smaller pool, so the required `Check Workflow Status` gates stay off it: a gate queued there holds up a finished build. +- **The pinned Ubuntu LTS arm label** runs the lightweight jobs that break any of those `ubuntu-slim` constraints or sit on the required-check path: PR and merge-queue change detection, `check-metadata`, the status gates, the docs quality gate, the snapshot publish, the Play upload, promotion and rollout, and the GitHub release. Shorter queue times than x64. +- **The pinned Ubuntu LTS x64 label** runs the Gradle-heavy jobs. Every single-runner job in `reusable-check.yml` pins it (`lint-check`, `screenshot-check`, `rb-check`, `test-shards`, `android-check`), as do release builds, Dokka and docs publishing. The `build-desktop` matrix job spans several runners instead, as does `verify-flatpak` in its own workflow. Pin for reproducibility. +- **Desktop runners:** a multi-OS matrix (macOS, Windows, and Ubuntu x64 and arm64) for the `build-desktop` job, `verify-flatpak`, and release packaging. Each matrix lists its own labels, which can trail the pinned LTS labels above. `.github/instructions/ci-workflows.instructions.md` restates the picking rule for anyone editing a workflow file. ### CI Gradle Properties -`gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses `.github/ci-gradle.properties`, which the `gradle-setup` composite action copies to `~/.gradle/gradle.properties`. Key CI overrides: +`gradle.properties` is tuned for local dev (8g heap, 6g Kotlin daemon). CI uses `.github/ci-gradle.properties`, which the `gradle-setup` composite action copies to `~/.gradle/gradle.properties`. Key CI overrides: - `org.gradle.daemon=false` (single-use runners) - `kotlin.incremental=false` (fresh checkouts) -- `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon +- `-Xmx4g` Gradle heap, `-Xmx6g` Kotlin daemon - VFS watching disabled, workers capped at 4 - `org.gradle.isolated-projects=true` for better parallelism -- Disables unused Android build features (`resvalues`, `shaders`) ### CI Conventions -- **KMP Smoke Compile:** `./gradlew kmpSmokeCompile` is a lifecycle task (registered in `RootConventionPlugin`) that auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks. -- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` checks `project.findProperty("ci") == "true"` and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`. +- **KMP Smoke Compile:** `./gradlew kmpSmokeCompile` is a lifecycle task (registered in `RootConventionPlugin`) that depends on `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` for every KMP module in the hand-maintained `ALL_MODULES_FULL` list, plus `assembleAndroidDeviceTest` for `:core:database` and `:core:model`, so a device-test APK that fails to dex or package fails here. `scripts/check-module-list.py` fails the PR when that list drifts from `settings.gradle.kts`. CI runs it in `shard-core`. +- **Kotlin warnings fail the test shards:** they pass `-PwarningsAsErrors=true`, which sets `allWarningsAsErrors` on every Kotlin compilation (`KotlinAndroid.kt`, plus `desktopApp` and `schema-strings`). The shards don't run the `compile*MainKotlinMetadata` tasks, so a warning only those report doesn't fail CI (today they warn about duplicate KLIB names). Reproduce locally with the same flag on the compile or test tasks you touched. +- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` reads the `ci` Gradle property (`providers.gradleProperty("ci")`) and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`. - **Detekt report formats:** Detekt.kt checks `project.findProperty("ci") == "true"` and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations. -- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{version}-sdk{level}` — update when bumping version or SDK level. +- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{os}-{arch}-{hash of gradle/libs.versions.toml}`, restoring from the `robolectric-{os}-{arch}-` prefix, so a catalog change that bumps Robolectric rolls the key without a hand edit. - **`mavenLocal()` gated:** Disabled by default to prevent CI cache poisoning. Pass `-PuseMavenLocal` for local JitPack testing. - **JUnit parallel execution:** Enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races. Cross-module parallelism comes from Gradle forks (`maxParallelForks`). +- **Test timeouts:** every Jupiter test and lifecycle method fails after 2 minutes (`junit.jupiter.execution.timeout.default`, `SEPARATE_THREAD` so code that ignores interrupts still fails by name), and every `Test` task stops after 15 minutes, which also covers the JUnit 4 host tests. Both live in `ProjectExtensions.kt`. - **Test retry:** Develocity plugin's native retry (`develocity.testRetry` on each Test task), configured in `ProjectExtensions.kt` (maxRetries=2, maxFailures=10). Screenshot tests opt out (maxRetries=0). The standalone `org.gradle.test-retry` plugin was removed. - **`fail-fast: false`:** Test sharding does not cancel other shards on failure. - **Explicit Gradle task paths:** Prefer `androidApp:lintFdroidDebug` over shorthand `lintDebug` in CI. -- **Pull request CI:** Main-only (`.github/workflows/pull-request.yml` targets `main`). -- **Merge queue hygiene:** `merge-queue.yml` cancels superseded runs for the same PR (GitHub does not auto-cancel destroyed merge-group runs) and skips the heavy pipeline for docs-only entries (`docs/**`, `*.md`). `rb-check` runs ONLY in the merge queue. `main-check.yml` passes `run_lint: false` — every main commit is a merge-queue-verified merge commit, so main pushes only rebuild the debug APKs for the snapshot release. -- **Cache writes:** Trusted on `main` only; merge-queue cache scopes are throwaway branches (writes unrecoverable), so the queue reads only, like all other refs. -- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.). -- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone. +- **Pull request CI:** `.github/workflows/pull-request.yml` runs on PRs into `main` and `release/**`. Its Gradle jobs skip the `scheduled-updates` and `scheduled-baseline` head branches; the merge queue still runs everything for them. +- **Merge queue hygiene:** `merge-queue.yml`'s `check-changes` job first cancels older runs for the same PR, best effort, because GitHub does not auto-cancel destroyed merge-group runs. It then lists the entry's files from the compare API, with no checkout, and skips the heavy pipeline for docs-only entries (`docs/**`, `fastlane/**`, `obtainium/**`, `*.md`), mirrored by `main-check.yml`'s `paths-ignore`. An entry of 300 or more files, the API's listing cap, runs full CI. Once the gate passes, `Check Workflow Status` posts the required `license/cla` on the group commit if the PR head has it, because cla-assistant.io only checks PRs. `rb-check` runs ONLY in the merge queue. `main-check.yml` passes `run_lint: false` because every main commit is a merge-queue-verified merge commit, so main pushes skip lint, `screenshot-check` and `rb-check`, and run the coverage shards, the debug APKs and the desktop packages for the snapshot release. Its concurrency group never cancels a started run: one run executes, one waits, and a newer push replaces only the waiting one. +- **Cache writes:** each cache has its own rule. setup-gradle's Gradle User Home cache is written by `reusable-check.yml` on `main` only (`GRADLE_CACHE_READ_ONLY`), so PRs and the merge queue only read it; outside that workflow, the Google, F-Droid and desktop release builds and `scheduled-baseline.yml` write it and every other caller reads. The Develocity remote build cache is pushed by `push` and `merge_group` builds and by `workflow_dispatch` runs on `main` that have the access key, never by PRs (`MeshtasticDevelocitySettingsPlugin`). `gradle-setup`'s Kotlin/Native (`~/.konan`) and Robolectric caches follow the same `cache_read_only` input: they save on a key miss only when it is not `'true'`, and a read-only run restores them without saving. Kotlin/Native is opt-in through `cache_konan`, which the test shards and the read-only docs builds pass, so only the test shards on `main` write it; `cache_robolectric` opts a job out of the Robolectric cache. +- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.). It runs three drift guards, each runnable locally with `python3`: `scripts/check-changes-filter.py` (every module root in `settings.gradle.kts` has a `/**` line in the `android` filter), `scripts/check-module-list.py` (`RootConventionPlugin`'s `ALL_MODULES_FULL` matches `settings.gradle.kts`) and `scripts/check-test-shards.py` (every module with tests is in a `reusable-check.yml` shard or exempted). +- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). `release.yml`'s Google and desktop build steps pass `-PaboutLibraries.release=true` to enable remote license fetching; the F-Droid build leaves it off so its output matches F-Droid's reproducible rebuild. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone. diff --git a/.specify/memory/agent-governance.md b/.specify/memory/agent-governance.md index 49badf9d38..7949f19c4a 100644 --- a/.specify/memory/agent-governance.md +++ b/.specify/memory/agent-governance.md @@ -35,7 +35,7 @@ full refresh also appends a ~95-line managed SPECKIT GOVERNANCE section to - Package manifest: `build.gradle.kts`, `settings.gradle.kts`, `gradle/libs.versions.toml` (`docs/Gemfile` + `docs/Gemfile.lock` belong to the Jekyll docs site only) - Task runners: Gradle wrapper (`gradlew`), convention plugins in `build-logic/` -- CI workflows: `.github/workflows/create-or-promote-release.yml`,`.github/workflows/dependency-graph-submit.yml` `.github/workflows/docs-deploy.yml`,`.github/workflows/docs-release.yml` `.github/workflows/main-check.yml`,`.github/workflows/merge-queue.yml` `.github/workflows/msstore-publish.yml`,`.github/workflows/post-release-cleanup.yml` `.github/workflows/pr-closed-cleanup.yml`,`.github/workflows/promote.yml` `.github/workflows/pull-request-target.yml`,`.github/workflows/pull-request.yml` `.github/workflows/release.yml`,`.github/workflows/reusable-check.yml` `.github/workflows/scheduled-baseline.yml`,`.github/workflows/scheduled-updates.yml` `.github/workflows/update-changelog.yml` `.github/workflows/verify-flatpak.yml`,`.github/workflows/winget-publish.yml` +- CI workflows: `.github/workflows/create-or-promote-release.yml`, `.github/workflows/docs-deploy.yml`, `.github/workflows/docs-link-check.yml`, `.github/workflows/docs-quality.yml`, `.github/workflows/docs-release.yml`, `.github/workflows/main-check.yml`, `.github/workflows/merge-queue.yml`, `.github/workflows/msstore-publish.yml`, `.github/workflows/play-listing.yml`, `.github/workflows/play-rollout.yml`, `.github/workflows/post-release-cleanup.yml`, `.github/workflows/pr-closed-cleanup.yml`, `.github/workflows/promote.yml`, `.github/workflows/protobufs-bump.yml`, `.github/workflows/pull-request-target.yml`, `.github/workflows/pull-request.yml`, `.github/workflows/release.yml`, `.github/workflows/reusable-check.yml`, `.github/workflows/scheduled-baseline.yml`, `.github/workflows/scheduled-updates.yml`, `.github/workflows/store-screenshots.yml`, `.github/workflows/update-changelog.yml`, `.github/workflows/verify-flatpak.yml`, `.github/workflows/version-bump.yml`, `.github/workflows/winget-publish.yml` - Source paths: `androidApp/`, `desktopApp/`, `core/`, `feature/`, `build-logic/`, `scripts/` - Test paths: `**/src/commonTest/`, `**/src/jvmTest/`, `**/src/androidHostTest/`, `screenshot-tests/`, `docs-screenshots/`, `baselineprofile/`, `core/konsist/` diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index d4a15a5830..67bee4a898 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,4 +1,50 @@ ### II. Zero Lint Tolerance @@ -198,15 +247,18 @@ Governance rules: from Crowdin (`crowdin.yml`) — never hand-edit a locale page; deleting an English page means deleting its locale copies in the same commit. -Verification tooling — also enforced in CI: `.github/workflows/docs-quality.yml` runs the -link check, the coverage check, a two-way `DocBundleLoader.kt` registry check, and the alias -registration check as a **blocking** gate on PRs touching `docs/en/**` (freshness stays -advisory). Run locally before pushing docs changes: +Verification tooling, also enforced in CI: `.github/workflows/docs-quality.yml` runs the +link check, the coverage check, a two-way `DocBundleLoader.kt` registry check, the alias +registration check, and a locale front matter check (`layout` and `nav_order` in every +`docs//` page match `docs/en/`) as a **blocking** gate on PRs touching `docs/en/**` +or any other `docs/**/*.md`, locale pages included (freshness stays advisory). Run locally +before pushing docs changes: ```bash node scripts/check-doc-coverage.js # every user-facing feature module has a page node scripts/validate-doc-links.js # internal cross-references and image paths resolve node scripts/check-doc-aliases.js # frontmatter aliases are registered in DocBundleLoader.kt +python3 scripts/docs/sync-locale-front-matter.py --check # locale layout/nav_order match docs/en; drop --check to restore node scripts/check-doc-freshness.js # advisory: pages >180 days old, or missing last_updated ``` @@ -290,4 +342,4 @@ summary derived from this constitution. The files `.github/copilot-instructions. Constitution Check confirming all seven principles were evaluated. Complexity violations require explicit justification in the Complexity Tracking table of the plan document. -**Version**: 1.4.0 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-09-15 +**Version**: 1.4.2 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-09-28 diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index 081fa7b3c6..cc34ffaebf 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -148,7 +148,8 @@ |-----------|--------|---------------| | `commonMain` | [New files / Modified files] | All business logic and UI | | `androidMain` | [None / Platform integration only] | [Justification if needed] | -| `jvmMain` | [None / Shared JVM code] | [Justification if needed] | +| `jvmMain` | [None / Desktop-only JVM code] | [Justification if needed] | +| `jvmAndroidMain` | [None / JVM code shared by Android and Desktop] | [Justification if needed] | ## Design Standards Compliance diff --git a/AGENTS.md b/AGENTS.md index af2a07523a..35ca97b58f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M - **Project Goal:** Decouple business logic from Android for multi-platform (Android, Desktop, iOS). - **Agent Memory:** `.agent_memory/` is local-only scratch (git-ignored) — never stage or commit it. Skim the top (most recent) entry of `.agent_memory/session_context.md` for current state — it is capped at ~5 entries; older handovers live in `session_context.archive.md` (read only if you need historical detail). -- **Skills Directory (CONSULT THESE FIRST):** +- **Skills Directory (CONSULT THESE FIRST):** each is also symlinked into `.claude/skills/`, which is the only path Claude Code discovers skills from; the files themselves live here. - `.skills/project-overview/` - Codebase map, namespacing, **Bootstrap Steps**. - `.skills/kmp-architecture/` - Expect/actual, source-sets, conventions. - `.skills/compose-ui/` - Adaptive UI, **String Resources (consult strings-index.txt first)**. @@ -27,7 +27,7 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M - **Memory Persistence:** Add a new entry to the TOP of `.agent_memory/session_context.md` at the end of every session or major task. Keep it capped at ~5 entries — move anything older to `session_context.archive.md`. - **Bootstrap First:** Run the mandatory bootstrap steps in `.skills/project-overview/SKILL.md` before any build. - **Plan Before Execution:** Use `.agent_plans/` (git-ignored) for complex refactors. -- **Baseline Verification:** Always run: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests` +- **Baseline Verification:** Always run: `./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests` diff --git a/CHANGELOG.md b/CHANGELOG.md index 6405211d7f..4d9c57a119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,72 @@ See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases) ## [Unreleased] -### Unreleased (not yet in any build) - -#### 🛠️ Fixes -* fix(messaging): keep the Enter key in the message composer by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7155 -* fix(storeforward): dedupe a router replay by original_id by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7158 -* fix(service): keep the process alive and awake through firmware updates and scans by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7159 -* fix(messaging): key conversations to channel identity, not slot index by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7162 - -### Open Beta (v2.8.2-open.2) -Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.8.1): +### Internal (v2.8.3-internal.4) +Changes since [`v2.8.2`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.8.2): #### 🏗️ Features +* feat(network): a hidden showcase scenario for Demo Mode by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7377 +* feat(app): shell-only debug launch switches for onboarding and the trust dialog by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7375 +* feat(settings): raise the coding rate over a modem preset by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7405 +* feat(firmware): show installed vs latest bootloader before an upgrade by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7409 +* feat(auto): notification messaging on Android Auto by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7417 +* feat(map-maplibre): start with collapsed attribution strip on seconda… by @Tha14 in https://github.com/meshtastic/Meshtastic-Android/pull/7424 +* perf(store-screenshots): wait for the map to draw instead of a fixed 45 seconds by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7501 +* feat(desktop): add draggable scrollbars to node and message lists by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7506 +* feat(notifications): post reactions on their own channel by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7507 +* feat(admin): add Reboot into DFU mode admin action for nRF52 nodes by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7504 +#### 🖥️ Desktop +* fix(mqtt): remove noop mqtt to allow the mqtt proxy to work on the desktop builds by @Tha14 in https://github.com/meshtastic/Meshtastic-Android/pull/7400 +* fix(notifications): put every notification on its own channel and tap target by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7415 +* fix(database): delete old mesh logs in bounded batches by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7423 +* fix: keep WiFi credentials, addresses and coordinates out of app logs by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7419 +* fix: data correctness fixes from the Android audit by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7429 +* fix(node): show the real traceroute map on desktop by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7425 +* refactor: keep one copy of shared code across platforms by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7432 +* fix(ui): localize UI strings, fix stale effect captures, one EmptyState by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7430 +#### 🛠️ Fixes +* fix(map): keep the node track map responsive on long tracks by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7373 +* fix(konsist): anchor the scanned-source inputs at the source roots by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7382 +* fix(map): frame the mesh clear of the map's own controls by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7385 +* fix(node): drop the filter bar's own background inside the list header by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7386 +* fix(appfunctions): index functions where AppSearch has no dynamic schema by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7402 +* fix(ui): split the link colour per mode by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7406 +* fix(settings): stop profile import dropping Mesh Beacon settings by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7416 +* fix(database): never publish a replacement pool while the write lock is held by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7418 +* fix(prefs): recover corrupt Android prefs files and make toggles atomic by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7420 +* fix(navigation): keep each tab's state across tab switches by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7421 +* fix(discovery): keep an unheard node's SNR distinct from 0 dB by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7422 +* fix(service): correct phone position units and omit missing readings by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7437 +* fix(wifi-provision): release the BLE peripheral on retry and on leaving by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7453 +* fix(map-maplibre): clear warnings and collect state with lifecycle by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7460 +* fix(takserver): log a route export that failed to write by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7459 +* fix(node): leave positions without a fix out of the GPX track by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7457 +* fix(service): stop the inbound pipeline waiting on node writes by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7464 +* fix(app): check a shared map file before importing it by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7463 +* fix(ui): show byte sizes in decimal units, formatted for the locale by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7469 +* fix(app): stop cleanly on devices the bundled SQLite can't run on by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7470 +* fix(service): keep the last-heard write off the inbound lock by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7474 +* fix(takserver): keep one route file across reinstalls by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7475 +* fix(ui): show transfer rates and file limits in decimal units by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7476 +* fix(ble): keep device addresses out of logs by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7478 +* fix(ble): export the bond wait receiver so bond broadcasts reach it by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7484 +* fix(firmware): show the percent while a maintenance UF2 downloads by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7485 +* fix(messaging): let a pinned conversation be unpinned by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7492 +* fix(app): restore the Apache HTTP legacy library for Google Maps by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7499 +* fix(metrics): break power chart lines across gaps in readings by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7505 +#### 📝 Other Changes +* refactor(data): page the log export, remove dedupe leftovers by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7449 +* refactor(map): share the Web Mercator projection with node clustering by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7483 + +## New Contributors +* @Tha14 made their first contribution in https://github.com/meshtastic/Meshtastic-Android/pull/7400 + + + + +## [2.8.2] - 2026-09-26 + +### 🏗️ Features * perf(ui): render QR codes at display density instead of fixed 960px by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6793 * feat: report Android 17 memory-limiter kills via ApplicationExitInfo by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6792 * feat(privacy): shield sensitive UI content from non-tool accessibility services by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6794 @@ -53,11 +107,35 @@ Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releas * feat(ui): share the link from the share dialog, and say what the dialog does by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7126 * feat(node): signed and verified indicators in place of the PKI lock by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7117 * fix(node): keep a contact's public key when a different one arrives by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7118 -#### 🖥️ Desktop +* feat(settings): show the license notice on the About screen by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7191 +* feat(map): clamp zoom when framing bounds, and let style images use hardware bitmaps by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7245 +* refactor(navigation): route deep links through navigation3 UriDeepLinkMatcher by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7246 +* refactor(settings): read numeric field bounds from the protobufs field registry by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7242 +* feat(telemetry): surface lightning, PM status and soil-water metrics by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7237 +* feat(node): rank maker hardware between supported and community in the device badge by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7263 +* feat(settings): gate config fields on the schema's firmware versions by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7262 +* feat(settings): hide module settings the node reports compiled out by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7272 +* feat(mqtt): surface a refused subscription from the broker by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7290 +* feat(settings): drive the module gates from the schema by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7296 +* feat(settings): label enum pickers from the schema by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7298 +* feat(settings): read the labels and helper text the schema already has by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7302 +* feat(beacon): honour the advertised frequency slot by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7305 +* feat(settings): emit the enum key prefixes the schema strings use by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7306 +* feat(search): one M3 search bar, settings search, and node status in search by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7303 +* feat(settings): show the unit the schema declares by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7304 +* feat(connections): handle hardware without Bluetooth for Android XR by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7318 +* feat(connections): handle hardware without USB host for Android XR by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7322 +* refactor(connections): give Demo Mode its own section by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7326 +* feat(messaging): show whether a delivery receipt was proven by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7284 +* feat(messaging): record signing and ack proof on reactions by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7339 +### 🖥️ Desktop * fix(desktop): disable macOS notifications when the process has no app bundle by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6876 * fix(desktop): test the bundle path, not the identifier, before notifying by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6885 * fix(desktop): use standard SPDX license identifier for RPM packaging by @RCGV1 in https://github.com/meshtastic/Meshtastic-Android/pull/7043 -#### 🛠️ Fixes +* fix(desktop): pin the Flathub screenshots to a commit that survives the squash by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7243 +* fix(desktop): keep MapLibre's FFI upcall methods through ProGuard by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7287 +* refactor(model): parse the selected device address once by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7328 +### 🛠️ Fixes * fix(navigation): clear deep-link replay cache once applied to the backstack by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6796 * fix(ui): give feedback when a contact or channel import arrives while disconnected by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6798 * fix(database): rebuild the packet FTS index after the schema-52 table recreation by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6808 @@ -102,17 +180,43 @@ Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releas * fix(nfc): write to tags that have never been NDEF-formatted by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7123 * fix(node): stop presenting MQTT-only nodes as unheard on the current LoRa by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7138 * fix(node): revert the time frame selector to a segmented row by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7142 -#### 📝 Other Changes +* fix(messaging): keep the Enter key in the message composer by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7155 +* fix(storeforward): dedupe a router replay by original_id by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7158 +* fix(service): keep the process alive and awake through firmware updates and scans by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7159 +* fix(messaging): key conversations to channel identity, not slot index by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7162 +* fix(ui): drop the lazy list cache windows that crash inside a lookahead scope by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7185 +* fix(settings): state lockdown's irreversibility in the enable dialog by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7171 +* fix(network): point the API base URL at the production host by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7197 +* fix(discovery): write a dwell and its parent check in one transaction by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7204 +* fix(map): keep the 300 ms ease on MapLibre camera nudges by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7240 +* fix(ble): replace deprecated preConflate with bufferCapacity by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7236 +* fix(model): fold 0xAE into the hash of an AEAD channel by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7239 +* fix(model): align the US first-setup preset with the device screen by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7244 +* fix(settings): name config exports after the long name, not the short name by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7248 +* fix(nodes): the direct filter no longer returns MQTT nodes by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7256 +* fix(mqtt): TLS switch shows and sets the stored flag for the public broker by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7255 +* fix(connection): name the transport in handshake stall reports by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7276 +* fix(mqtt): subscribe with the options the negotiated protocol version allows by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7273 +* fix(analytics): give crash reports the radio that produced them, a real blame frame, and a ceiling by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7274 +* fix(settings): write the external notification duration in milliseconds by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7307 +* fix(node): keep the node counts visible while searching by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7311 +* fix(ble): don't arm a BLE transport on hardware without Bluetooth by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7321 +* fix(connections): refuse serial addresses without USB host by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7327 +* fix(service): stop a stale saved address overwriting a newer selection by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7329 +* fix(service): stay foreground only for an address that can connect by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7330 +* fix(messaging): clarify the message filter controls by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7324 +* fix(notifications): give bubbles an adaptive icon by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7336 +* fix(position): send no coordinates with a position request when we have none by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7361 +### 📝 Other Changes * refactor(settings): edit the status message on the user screen by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6951 * refactor(map): retire the maps-utils workarounds its 5.1 fixes made stale by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7046 * refactor(prefs): keep each surface's filters in one state object by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7120 +* refactor(appfunctions): migrate to AppFunctionServiceEntryPoint by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7340 ## New Contributors * @azchohfi made their first contribution in https://github.com/meshtastic/Meshtastic-Android/pull/6864 * @gargomoma made their first contribution in https://github.com/meshtastic/Meshtastic-Android/pull/7119 - - ## [2.8.1] - 2026-08-20 diff --git a/CLAUDE.md b/CLAUDE.md index e5fb54af87..8d52542bcd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Claude-Specific Instructions -- **Skills:** Load only the `.skills/` module relevant to the current task — don't read them all. Start with `.skills/project-overview/SKILL.md` (codebase map, bootstrap, troubleshooting). +- **Skills:** Load only the `.skills/` module relevant to the current task — don't read them all. Start with `.skills/project-overview/SKILL.md` (codebase map, bootstrap, troubleshooting). Each module is symlinked into `.claude/skills/` so it can also be selected by name, since that is the only directory Claude Code discovers skills from. - **Plan Mode:** Use it for changes spanning multiple modules; write plans to `.agent_plans/` (git-ignored). - **Delegate to keep context lean** (this is a 20+ module KMP repo): - **Broad searches** ("where is X used", "find all implementers of Y") → dispatch the `Explore` subagent so file dumps stay out of the main context; you get back the conclusion. - **Gradle builds/tests/lint** → dispatch the `gradle-runner` subagent. A full `assembleDebug`/`allTests` log is thousands of lines; the subagent returns only pass/fail + failing tests. Don't run heavy `./gradlew` tasks inline. - - **Symbol navigation** ("where is this defined", "who calls this", "find implementers") → use the `LSP` tool (`goToDefinition` / `findReferences` / `goToImplementation`) instead of reading whole files. `kotlin-language-server` is supplied by the `nixtastic` plugin; this repo configures nothing. If the `LSP` tool answers "No LSP server available for file type: .kt", the plugin is not loaded or the binary is not on PATH — there is nothing to fix in this repo. + - **Symbol navigation** ("where is this defined", "who calls this", "find implementers") → `rg` for the symbol, then read only the matching range. There is no Kotlin LSP server: none available today handles KMP reliably, so the `LSP` tool answering "No LSP server available for file type: .kt" is expected. - **Big files are guarded, not free:** `.claude/settings.json` denies the Crowdin locale `strings.xml` files and prompts before reading the base `strings.xml`, `firmware_releases.json`, `emoji-data.json`, and `flatpak-sources.json`. For strings, consult `.skills/compose-ui/strings-index.txt` instead of the raw file. ## Quick Reference @@ -29,13 +29,13 @@ want real Google Maps tiles (`MAPS_API_KEY=…`). `local.properties` is not read **Baseline verification — run before every push** (CI has failed on skipped local checks): ```bash -./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests +./gradlew spotlessApply spotlessCheck detekt detektTypeResolved assembleDebug test allTests ``` -Both `test` and `allTests` are required: `allTests` covers KMP modules (where the bare `test` task is ambiguous and silently skips), `test` covers pure-Android/JVM modules. Add `kmpSmokeCompile` when touching a KMP module. After adding string resources, run `python3 scripts/sort-strings.py`. Change-type matrix and CI architecture: `.skills/testing-ci/SKILL.md`. +Both `test` and `allTests` are required: `allTests` runs each KMP module's `jvmTest` and Android host tests (a KMP module has no `test` task, and naming `:core:data:test` fails as ambiguous), `test` covers pure-Android/JVM modules and skips KMP ones. Add `kmpSmokeCompile` when touching a KMP module. After adding string resources, run `python3 scripts/sort-strings.py`. Change-type matrix and CI architecture: `.skills/testing-ci/SKILL.md`. **Single test:** ```bash ./gradlew :feature:messaging:allTests # one KMP module ./gradlew :androidApp:testFdroidDebugUnitTest # one Android/JVM module -./gradlew :core:data:allTests --tests "*PacketHandlerTest*" # filter to one class/method +./gradlew :core:data:jvmTest --tests "*PacketHandlerTest*" # filter to one class/method (allTests takes no --tests) ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 266a42e2b4..2a36d594c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,10 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co - **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`. - Do **not** use the legacy `androidApp/src/main/res/values/strings.xml`. - **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`. + - **Schema strings:** every label and description in the protobufs field metadata is generated into + `values/schema_strings.xml`, keyed by schema path (`Res.string.schema_lora_hop_limit`). A settings control + that edits one protobuf field uses that key. Do not edit the file or write a `schema_` key by hand + (`./gradlew :schema-strings:sync` regenerates it); wrong wording is a change to `meshtastic/protobufs`. - **Usage:** ```kotlin import org.jetbrains.compose.resources.stringResource @@ -36,7 +40,7 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co Meshtastic-Android uses [Detekt](https://detekt.dev/) for static code analysis and linting of Kotlin code. -- Run `./gradlew detekt` before submitting your pull request to ensure your code passes all lint checks. +- Run `./gradlew detekt detektTypeResolved` before submitting your pull request to ensure your code passes all lint checks. `detektTypeResolved` runs the rules that need the compile classpath, which plain `detekt` skips. - Fix any Detekt warnings or errors reported in your code. - Suppress individual warnings only as a last resort. - You can find Detekt configuration in the `config/detekt` directory. If you believe a rule should be changed or suppressed, discuss it in your PR. @@ -55,7 +59,7 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t - Ensure all tests pass by running: - `./gradlew test` for unit and Robolectric tests (pure-Android modules) - `./gradlew allTests` for KMP module tests (`core:*`, `feature:*`) — neither `test` nor `allTests` alone is sufficient; both must pass. - - `./gradlew kmpSmokeCompile` when touching any KMP module — compiles the non-Android targets the unit tests don't cover + - `./gradlew kmpSmokeCompile` when touching any KMP module, to compile the non-Android targets the unit tests don't cover and assemble the device-test APKs - `./gradlew connectedAndroidTest` for instrumented tests - For UI components, write Robolectric Compose tests where possible for faster execution. - If your change is difficult to test, explain why in your pull request. diff --git a/README.md b/README.md index 35e11e982f..3328dd0990 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ What those two channels point at right now: | Channel | Currently | Released | |---|---|---| -| **Latest release** | `v2.8.1` | 2026-08-20 | -| **Open beta** | `v2.8.2-open.2` | 2026-09-13 | +| **Latest release** | `v2.8.2` | 2026-09-25 | +| **Open beta** | *none published right now* | — | @@ -84,7 +84,7 @@ The two documentation sites below are deployed to GitHub Pages automatically on | Site | URL | Contents | |---|---|---| -| **User & Developer Docs** | [meshtastic.github.io/Meshtastic-Android](https://meshtastic.github.io/Meshtastic-Android/) | Jekyll site — user guide, developer guide, in-app doc content | +| **User & Developer Docs** | [meshtastic.github.io/Meshtastic-Android](https://meshtastic.github.io/Meshtastic-Android/) | Jekyll site with the user and developer guides | | **API Reference** | [meshtastic.github.io/Meshtastic-Android/api](https://meshtastic.github.io/Meshtastic-Android/api/) | Dokka-generated KDoc for all public APIs | ### Generating Locally @@ -98,7 +98,7 @@ BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll serve \ **API Reference (Dokka):** ```bash -./gradlew dokkaGeneratePublicationHtml +./gradlew :dokkaGeneratePublicationHtml # Output: build/dokka/html/index.html ``` diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 6efb40bc97..66107cbd34 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -8,7 +8,7 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr - **Trigger:** To start a new release or promote an existing one, a developer runs the workflow from the GitHub Actions tab. - **Inputs:** The workflow requires the following inputs: - 1. `base_version`: The base version number you are releasing (e.g., `2.8.0`). + 1. `base_version`: The base version number you are releasing (e.g., `2.8.0`). It must be `X.Y.Z` and equal `VERSION_NAME_BASE` in `config.properties` at the commit being released (`HEAD` for an internal cut, the promoted tag's commit for a promotion), or the run stops before any tag is pushed. 2. `channel`: The release channel you are targeting (`internal`, `closed`, `open`, or `production`). 3. `dry_run`: If `true`, calculates the tag but does not push it or start the release (default: `false`). 4. `no_review_in_flight`: **Promotions only, and a hard gate.** Before promoting, check @@ -18,12 +18,18 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr already in flight. Internal releases and dry runs are exempt (Play internal testing skips full review). - **Automation:** The workflow handles everything automatically: - - **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes; a production release also opens a PR folding the same notes into `CHANGELOG.md`. Between releases that file is only refreshed by dispatching the `Update Changelog` workflow by hand. - - **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Promotions skip this entirely and retag the already-built artifact (see below). - - **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. + - **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes. The internal draft's notes cover the PRs since the previous published pre-release; a production promotion rewrites them over the whole range since the previous production tag, with the metainfo `` for the version as a Highlights section on top, and opens a PR folding the same notes into `CHANGELOG.md`. Every internal, closed and open run ends by dispatching the `Update Changelog` workflow, which refreshes that file's `[Unreleased]` section through its own PR. Every PR the release automation opens (these two, the screenshot refresh and the version bump) goes through `.github/actions/bot-pr` with `CROWDIN_GITHUB_TOKEN`, so it runs the normal PR checks and merges itself through the queue once they pass. + - **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Once `publish-play` has uploaded the bundle the tag stays, since Play keeps that versionCode; re-run the failed jobs instead. Promotions skip this entirely and retag the already-built artifact (see below). + - **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. An internal cut sends the bundle to Play only after every Android, desktop and Flatpak leg has built, so a failed leg and its deleted tag leave nothing on Play. Each promotion also uploads the Play "What's new" text for every locale from `fastlane/metadata/android//changelogs/default.txt`, which `scripts/sync-play-changelog.py` renders from the metainfo `` and Crowdin translates. + - **Captures the store screenshots** *(internal releases)*: `store-screenshots.yml` runs the real debug apps from the tag, connected to Demo Mode's showcase mesh, on one emulator per flavor and on a virtual display for desktop, and attaches `store-listing-screenshots-google-.zip`, `store-listing-screenshots-fdroid-.zip` and the five desktop PNGs to the draft once it exists, so the draft does not wait on the capture. A set with a shot missing is replaced whole by the committed one, so the metainfo's screenshot URLs still resolve, and the job summary says which set went up. A failed capture never fails the release. + - **Publishes the Play listing:** every promotion runs the `play_listing` lane with the tag's text for every locale and the google-flavor screenshots, as a dry run on closed and open and for real on production, held as "changes not sent for review". Production also opens a self-merging PR that writes the fdroid-flavor screenshots back into `fastlane/`, which F-Droid and IzzyOnDroid read from git, and the desktop set into `desktopApp/packaging/linux/screenshots/`. + - **Publishes docs:** Every promotion dispatches `docs-release.yml` on the new tag (the tag is created with `GITHUB_TOKEN`, so its tag trigger never fires on its own). + - **Refreshes the Obtainium table:** Every promotion dispatches `scheduled-updates.yml` once the release is published, so the Obtainium table in `README.md` follows it without waiting for the scheduled run. + - **Writes a checklist:** The promotion run's summary lists what it did, what it dispatched, and what is still done by hand. winget, the Microsoft Store, Homebrew and Flathub each read "will skip: `` unset" when their secret is missing, since those workflows and jobs then submit nothing. + - **Reruns:** Re-running a failed `update-github-release` job finds the release under its final tag when the first attempt already moved it, and updates the bot PRs in place; its workflow dispatches run again. The Discord announcement goes out whenever the release step succeeded, whatever failed after it. - **Deploys Desktop** *(internal releases)*: Builds native installers (DMG, MSI, EXE, DEB, RPM, AppImage) and Flatpak sources on a matrix of runners and attaches them to the GitHub Release. - **Changelog:** Both the GitHub Release notes and `CHANGELOG.md` are generated from merged PR labels, not raw commit messages — label PRs correctly (`enhancement`, `bugfix`, etc.) to keep them accurate. -- **Not part of this workflow:** Firmware/hardware/device-links lists and Crowdin translations are kept current by a separate hourly workflow, `scheduled-updates.yml` ("Scheduled Updates (Firmware, Hardware, Translations)"), which opens its own PR rather than committing directly — it never runs as part of a release. `VERSION_NAME_BASE` in `config.properties` is likewise never written by automation: a maintainer bumps it by hand in an ordinary PR (e.g. "chore: bump VERSION_NAME_BASE to 2.8.2 (#6820)") before starting a release for a new base version, paired with a matching `` entry in `desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml` — a `pull-request.yml` check fails the PR if that entry is missing. `Create or Promote Release` only *reads* `VERSION_NAME_BASE`/`VERSION_CODE_OFFSET` from `config.properties` to compute the build's version name/code. +- **Not part of this workflow:** Firmware/hardware/device-links lists and Crowdin translations are kept current by a separate scheduled workflow, `scheduled-updates.yml` ("Scheduled Updates (Firmware, Hardware, Translations)"), which opens its own PR rather than committing directly and enables auto-merge on it, so it lands through the merge queue once its checks pass. A promotion dispatches it (above) without waiting on it. `VERSION_NAME_BASE` in `config.properties` moves to the next patch version after each production release: `promote.yml` dispatches `version-bump.yml`, which runs `scripts/bump-version-name.py` and opens a self-merging PR carrying the new `` entry in `desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml`, its five `` URLs moved to `releases/download/v/`, and `fastlane/metadata/android/en-US/changelogs/default.txt` rendered from that entry. `pull-request.yml` fails a bump PR missing any of them; the bot PR is opened with `CROWDIN_GITHUB_TOKEN`, so those checks run on it and the merge queue takes it. The entry's paragraph is a placeholder; replace it and re-run `scripts/sync-play-changelog.py` before the next internal cut, because it becomes the release Highlights and Play's "What's new". A minor or major line is a hand PR running the same script, and the workflow skips when `main` is already past the shipped version. `Create or Promote Release` only *reads* `config.properties` at the commit being released: `VERSION_NAME_BASE` must match `base_version`, and `VERSION_CODE_OFFSET` plus that commit's count gives the build's version code. ## Release Steps @@ -38,9 +44,9 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr The workflow will: 1. **Tag** the current commit on the branch with an incremental internal tag (e.g., `v2.8.0-internal.1`) — no new commit is created; it tags whatever is already at `HEAD`. -2. **Build & Deploy** the built Android artifact to the Play Store Internal track. -3. **Build Desktop** native installers and Flatpak sources on macOS, Windows, and Linux runners. -4. Publish a **draft** pre-release on GitHub with all artifacts attached. It stays a draft until +2. **Build** the Android bundle and APKs, and the desktop installers and Flatpak sources on macOS, Windows, and Linux runners. +3. **Deploy** the Android bundle to the Play Store Internal track once every build has succeeded. +4. Publish a **draft** pre-release on GitHub with all artifacts attached; the store screenshots follow when the capture finishes. It stays a draft until the first promotion (closed/open/production), at which point `promote.yml` un-drafts the *same* release object (retagging it to the new channel's tag) rather than creating a new one. @@ -63,14 +69,42 @@ After testing is complete on all pre-release channels, you can create the final ### 4. Post-Release -1. **Verify Android:** Check the Google Play Console to ensure the build is available on the correct track. +Start from the promotion run's summary: it lists what the run did and dispatched, and what +remains by hand. + +1. **Verify Android:** Check the Google Play Console to ensure the build is available on the correct track. A production promotion starts a staged rollout at 10% (open at 50%); widen, complete or halt it with the **`Play Rollout`** workflow (see Staged Rollout below). 2. **Verify Desktop:** Download and smoke-test at least one installer (DMG, MSI, or AppImage) from the GitHub Release. 3. **Verify the desktop store submissions** *(production only — see below)*: the Microsoft Store submission in Partner Center, and the pull request opened against `microsoft/winget-pkgs`. -4. **Merge:** If a `release/*` branch was used for stabilization (CI runs the same PR checks + Each store workflow warns in its summary when its secrets are not set and it submitted nothing, + and the promotion checklist already says so. +4. **Flathub** *(production only)*: merge the `update-flathub` PR in `flathub/org.meshtastic.MeshtasticDesktop` once Flathub's test build passes, or bump it by hand when `FLATHUB_TOKEN` is unset (see Flatpak below). +5. **Post-Release Cleanup** *(production only)*: `Docs Release` dispatches `post-release-cleanup.yml` with `confirm_deletion: true` once it has published `/vX.Y.Z/`, deleting the pre-releases, tags and docs snapshots at or below `X.Y.Z` and every production docs copy older than the newest one below `X.Y.Z`. Check that run; a manual dispatch is the retry and defaults to a dry run. +6. **Next version line** *(production only)*: the `version-bump.yml` PR bumps `VERSION_NAME_BASE` and merges itself. Replace its placeholder `` before the next internal cut. +7. **Merge:** If a `release/*` branch was used for stabilization (CI runs the same PR checks against PRs targeting `release/**` as it does for `main`), merge it back into `main` now that production has shipped. +### Staged Rollout + +**`Play Rollout`** (`play-rollout.yml`) changes the one `inProgress` release on the `production` +or `beta` track. Its inputs are the `track`, an `action` and, for `rollout`, a `fraction`: + +| Action | Effect | +|---|---| +| `rollout` | Widens the release to `fraction`, which must be above the current fraction and below 1 | +| `complete` | Ships the release to every user | +| `halt` | Stops the rollout at its current fraction | + +It carries the same `no_review_in_flight` gate as a promotion, because a committed change +cancels and restarts any review in flight. It runs in its own concurrency group, not +`Create or Promote Release`'s, since a group keeps one pending run and a rollout queued there +would cancel a pending promotion. The run stops without +changing anything unless the track holds exactly one `inProgress` release, and it verifies the +new status and fraction on the track afterwards. A halted release is resumed or completed in the +Play Console, since `supply` only acts on `inProgress` releases. When Play will not send a change +for review on its own, the change waits under Publishing overview in the Play Console. + ### Desktop Store Publishing (production only) Publishing a **production** release also fires two workflows, both keyed on the GitHub @@ -119,7 +153,7 @@ Desktop uses the same version resolution chain as Android — both read `VERSION ### Flatpak -Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and handles JBR bundling; the AppStream metainfo and `.desktop` entry it installs come from this repo, out of the tag it builds. So the desktop screenshots and the `` notes ship with the tag - nothing to do on the Flathub side beyond the version bump. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README). +Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and handles JBR bundling; the AppStream metainfo and `.desktop` entry it installs come from this repo, out of the tag it builds. So the `` notes ship with the tag, and the `` URLs name the desktop PNGs the internal cut attached to that version's release (`releases/download/v/`), which Flathub's guidelines allow and a branch link would not. A production promotion's `update-flathub` job opens the bump PR with `FLATHUB_TOKEN`, and skips with a notice when that secret is unset. The PR moves four things together: the tag and commit, the Gradle distribution zip URL and sha256 (from the tag's `gradle/wrapper/gradle-wrapper.properties`), and the release's `flatpak-sources.json` asset. `scripts/verify-flatpak/bump-flathub-manifest.py` rewrites those manifest fields in place and fails, leaving the bump to be done by hand, when any of them no longer matches exactly once. The JBR, the runtime and the patches are left alone; Flathub's test build on the PR checks them against the tag. flathubbot's zip-bump PRs follow the latest Gradle rather than the tag's wrapper and fail their test build; close them rather than merge them. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README), and `verify-flatpak.yml` builds them offline nightly and on changes to that directory, the workflow or the Gradle wrapper. ## Build Attestations & Provenance diff --git a/androidApp/README.md b/androidApp/README.md index 94ec2d80b9..dddba681f9 100644 --- a/androidApp/README.md +++ b/androidApp/README.md @@ -25,7 +25,6 @@ The module primarily serves as a "glue" layer, connecting: ```mermaid graph TB :androidApp[androidApp]:::android-application - :androidApp -.-> :baselineprofile :androidApp -.-> :feature:map-maplibre :androidApp -.-> :feature:map-terrain :androidApp -.-> :core:ble diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 0d432101b4..2bd4338f48 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -124,8 +124,6 @@ configure { ) } ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a") } - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } // Disable ABI splits for bundle builds or when explicitly requested via Gradle property. @@ -174,6 +172,9 @@ configure { // what a real build carries comes from the plugin. See #6883. manifestPlaceholders["MAPS_API_KEY"] = "dummy" } + if (name == "fdroid") { + proguardFile("proguard-rules-fdroid.pro") + } } } @@ -197,24 +198,22 @@ secrets { propertiesFileName = "secrets.properties" } -ksp { arg("appfunctions:aggregateAppFunctions", "true") } +// AppSearch without dynamic-schema support indexes only the v1 XML named by the `android.app.appfunctions` property. +ksp { arg("appfunctions:generateV1Xml", "true") } + +// Merging into src/main is what ships the profile in fdroid too. +baselineProfile { mergeIntoMain = true } + +// The producer only has the google flavor, so only googleRelease may depend on it: fdroidRelease would fail to resolve +// it. The plugin creates this configuration per variant, after this script runs. +configurations + .matching { it.name == "googleReleaseBaselineProfile" } + .configureEach { dependencies.add(projects.baselineprofile) } androidComponents { onVariants(selector().withBuildType("debug")) { variant -> variant.flavorName?.let { flavor -> variant.applicationId.set("com.geeksville.mesh.$flavor.debug") } } - - onVariants(selector().withBuildType("release")) { variant -> - if (variant.flavorName == "google") { - val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() } - val minifyTaskName = "minify${variantNameCapped}WithR8" - val uploadTaskName = "uploadMapping$variantNameCapped" - // Use tasks.names to check existence without eagerly realizing tasks - if (tasks.names.contains(uploadTaskName) && tasks.names.contains(minifyTaskName)) { - tasks.named(minifyTaskName).configure { finalizedBy(uploadTaskName) } - } - } - } } dependencies { @@ -348,8 +347,4 @@ dependencies { testImplementation(libs.androidx.glance.appwidget) // JVM variant provides the host-platform native library for BundledSQLiteDriver under Robolectric testRuntimeOnly(libs.androidx.sqlite.bundled.jvm) - - // Producer of the baseline profile consumed by the release build. The androidx.baselineprofile - // plugin merges the generated rules into src//generated/baselineProfiles at build time. - baselineProfile(projects.baselineprofile) } diff --git a/androidApp/detekt-baseline.xml b/androidApp/detekt-baseline.xml index 0f71fbcb08..832f8be03f 100644 --- a/androidApp/detekt-baseline.xml +++ b/androidApp/detekt-baseline.xml @@ -4,53 +4,42 @@ ComplexCondition:MapViewModel.kt:MapViewModel$name.isBlank() || (urlTemplate.isBlank() && localUri == null) || (localUri == null && !isValidTileUrlTemplate(urlTemplate)) ComplexCondition:MapViewModel.kt:MapViewModel$normalized.name.isBlank() || (normalized.urlTemplate.isBlank() && normalized.localUri == null) || (normalized.localUri == null && !isValidTileUrlTemplate(normalized.urlTemplate)) - ComposableParamOrder:MapView.kt:@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MapsDialog - CyclomaticComplexMethod:DiscoveryGoogleMap.kt:@OptIn(MapsComposeExperimentalApi::class) @Composable fun DiscoveryGoogleMap - CyclomaticComplexMethod:DiscoveryOsmMap.kt:@Composable fun DiscoveryOsmMap - CyclomaticComplexMethod:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$override fun answerStream: kotlinx.coroutines.flow.Flow<AIDocAssistantResult> - CyclomaticComplexMethod:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$private fun buildContext: ContextResult - CyclomaticComplexMethod:TracerouteOsmMap.kt:@Composable fun TracerouteOsmMap - LambdaParameterInRestartableEffect:LocationHandler.kt:onPermissionResult: (Boolean) -> Unit - LambdaParameterInRestartableEffect:TracerouteOsmMap.kt:onMappableCountChange: (shown: Int, total: Int) -> Unit LongMethod:DiscoveryGoogleMap.kt:@OptIn(MapsComposeExperimentalApi::class) @Composable fun DiscoveryGoogleMap - LongMethod:DiscoveryOsmMap.kt:@Composable fun DiscoveryOsmMap LongMethod:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$override fun answerStream: kotlinx.coroutines.flow.Flow<AIDocAssistantResult> - LongMethod:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$override suspend fun isSupported: Boolean LongMethod:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$private fun buildPrompt: String - LongMethod:NodeTrackOsmMap.kt:@Composable fun NodeTrackOsmMap - LongMethod:TracerouteOsmMap.kt:@Composable fun TracerouteOsmMap - LoopWithTooManyJumpStatements:FdroidMapOverlayRenderer.kt:FdroidMapOverlayRenderer$for LoopWithTooManyJumpStatements:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$for MatchingDeclarationName:MapView.kt:GoogleMapMode - ModifierMissing:DownloadButton.kt:@Composable fun DownloadButton ModifierMissing:Main.kt:@Composable fun MainScreen - ModifierMissing:NodeMapScreen.kt:@Composable fun NodeMapScreen ModifierMissing:WaypointMarkers.kt:@OptIn(MapsComposeExperimentalApi::class) @Composable fun WaypointMarkers ReturnCount:GooglePlatformAnalytics.kt:GooglePlatformAnalytics.CrashlyticsLogWriter$override fun log ReturnCount:MapViewModel.kt:MapViewModel$fun getTileProvider: TileProvider? ReturnCount:MlKitDocTranslator.kt:MlKitDocTranslator$override suspend fun translatePage: TranslationResult ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun downloadLanguageModels: DownloadResult ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun translate: TranslationResult - SwallowedException:MapViewModel.kt:MapViewModel$e: Exception - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getChannelInfo: GetChannelInfoResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getDeviceStatus: GetDeviceStatusResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getMeshMetrics: GetMeshMetricsResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeDetails: GetNodeDetailsResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeList: GetNodeListResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getRecentMessages: GetRecentMessagesResponse - ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun sendMessage: SendMessageResponse - TooGenericExceptionCaught:FdroidMapOverlayRenderer.kt:FdroidMapOverlayRenderer$e: Exception + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getChannelInfo: GetChannelInfoResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getDeviceStatus: GetDeviceStatusResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getMeshMetrics: GetMeshMetricsResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeDetails: GetNodeDetailsResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeList: GetNodeListResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getRecentMessages: GetRecentMessagesResponse + ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun sendMessage: SendMessageResponse TooGenericExceptionCaught:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$e: Exception TooGenericExceptionCaught:MapView.kt:e: Exception - TooGenericExceptionCaught:MapView.kt:ex: Exception TooGenericExceptionCaught:MapViewModel.kt:MapViewModel$e: Exception TooGenericExceptionCaught:MlKitDocTranslator.kt:MlKitDocTranslator$e: Exception TooGenericExceptionCaught:MlKitMessageTranslator.kt:MlKitMessageTranslator$e: Exception - TooGenericExceptionCaught:SqlTileWriterExt.kt:SqlTileWriterExt$e: Exception TooManyFunctions:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant : AIDocAssistant TooManyFunctions:MapView.kt:org.meshtastic.app.map.MapView.kt - TooManyFunctions:MapViewModel.kt:MapViewModel : BaseMapViewModel - UtilityClassWithPublicConstructor:CustomTileSource.kt:CustomTileSource + UnnecessaryLaunchedEffect:Main.kt:LaunchedEffect + UnnecessaryLaunchedEffect:MapView.kt:LaunchedEffect + UnnecessaryLaunchedEffect:TracerouteMap.kt:LaunchedEffect + UnnecessaryLaunchedEffect:WaypointMarkers.kt:LaunchedEffect + UseOrEmpty:MapView.kt:RenderedMapLayer$dataLayer.properties["groundOverlays"] as? List<KmlGroundOverlay> ?: emptyList() + UseOrEmpty:MapView.kt:RenderedMapLayer$dataLayer.properties["images"] as? Map<String, Bitmap> ?: emptyMap() + UseOrEmpty:MapView.kt:mode.overlay?.forwardRoute?.mapNotNull { nodeLookup[it]?.position?.toLatLng() } ?: emptyList() + UseOrEmpty:MapView.kt:mode.overlay?.returnRoute?.mapNotNull { nodeLookup[it]?.position?.toLatLng() } ?: emptyList() + UseOrEmpty:MapView.kt:tracerouteSelection?.nodesForMarkers ?: emptyList() + UseOrEmpty:MapViewModel.kt:MapViewModel$uri.path ?: "" ViewModelForwarding:MapView.kt:CustomTileProviderManagerSheet(mapViewModel = mapViewModel) ViewModelForwarding:MapView.kt:MainMapContent( nodeClusterItems = nodeClusterItems, mapFilterState = mapFilterState, navigateToNodeDetails = navigateToNodeDetails, displayableWaypoints = displayableWaypoints, myNodeNum = myNodeNum, onEditWaypointRequest = { editingWaypoint = it }, onDeleteWaypointRequest = { deletingWaypoint = it }, onShowGeofenceInfo = { geofenceInfoWaypoint = it }, selectedWaypointId = selectedWaypointId, mapLayers = mapLayers, layerOpacity = layerOpacity, mapViewModel = mapViewModel, cameraPositionState = cameraPositionState, coroutineScope = coroutineScope, onShowClusterItemsDialog = { showClusterItemsDialog = it }, ) ViewModelForwarding:MapView.kt:MapLayerOverlay(layerItem, opacity, mapViewModel) diff --git a/androidApp/proguard-rules-fdroid.pro b/androidApp/proguard-rules-fdroid.pro new file mode 100644 index 0000000000..9e885f509b --- /dev/null +++ b/androidApp/proguard-rules-fdroid.pro @@ -0,0 +1,3 @@ +# F-Droid has no crash backend to retrace an obfuscated stack, so its builds keep +# their names. Play's DEX optimization check only sees the google flavor. +-dontobfuscate diff --git a/androidApp/proguard-rules.pro b/androidApp/proguard-rules.pro index d782b392ad..b4a18e50a4 100644 --- a/androidApp/proguard-rules.pro +++ b/androidApp/proguard-rules.pro @@ -1,8 +1,10 @@ # ============================================================================ # Meshtastic Android — ProGuard / R8 rules for release minification # ============================================================================ -# Open-source project: obfuscation is disabled (readable stack traces). We rely -# on R8 optimization + tree-shaking (unused code removal) for APK size reduction. +# Release builds are shrunk and optimized. The google flavor is also +# obfuscated; its mapping goes to Crashlytics and Datadog and is attached to +# each GitHub release. The fdroid flavor adds proguard-rules-fdroid.pro, which +# keeps it unobfuscated. # # Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room, # Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3, @@ -14,11 +16,7 @@ # ---- General ---------------------------------------------------------------- -# Open-source — no need to obfuscate --dontobfuscate - -# R8 optimization is ENABLED. Obfuscation stays off (-dontobfuscate above), so -# stack traces remain readable; tree-shaking plus the full optimization pass +# R8 optimization is ENABLED: tree-shaking plus the full optimization pass # (method inlining, class merging, Composer/ComposerImpl devirtualization, # unused-argument removal) all run. # @@ -39,6 +37,29 @@ # for auditing. Inspect this file after a release build to see what libraries inject. -printconfiguration build/outputs/mapping/r8-merged-config.txt +# ---- Names read at runtime -------------------------------------------------- +# Each name below is looked up by string, so obfuscation must leave it alone. + +# KableGattCacheRefresh reads these private Kable fields by reflection. +-keepclassmembernames class com.juul.kable.BluetoothDeviceAndroidPeripheral { + kotlinx.coroutines.flow.MutableStateFlow connection; +} +-keepclassmembernames class com.juul.kable.Connection { + android.bluetooth.BluetoothGatt gatt; +} + +# rumViewName() reports a route's class name as its Datadog view name. +-keepnames class * implements androidx.navigation3.runtime.NavKey + +# isDeprecatedEnumEntry() finds each constant's field by name to read @Deprecated. +-keepclassmembernames enum org.meshtastic.** { + ; +} + +# GooglePlatformAnalytics drops logging frames from Crashlytics stacks by class-name prefix. +-keepnames class org.meshtastic.app.analytics.GooglePlatformAnalytics* +-keepnames class co.touchlab.kermit.** + # ---- Networking (transitive references from Ktor on Android) ---------------- -dontwarn org.conscrypt.** diff --git a/androidApp/src/debug/AndroidManifest.xml b/androidApp/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..f8c6eee080 --- /dev/null +++ b/androidApp/src/debug/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/androidApp/src/fdroid/AndroidManifest.xml b/androidApp/src/fdroid/AndroidManifest.xml index adb8c226e3..d8d96da9ac 100644 --- a/androidApp/src/fdroid/AndroidManifest.xml +++ b/androidApp/src/fdroid/AndroidManifest.xml @@ -19,15 +19,6 @@ - - - + + + + + + + + + + + + + android:value="meshtastic_app_function_service-v1.xml" /> - - - - - - - - - + android:value="meshtastic_app_function_service.xml" /> diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt b/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt index 379127456d..e40a8ebc5c 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt @@ -16,36 +16,20 @@ */ package org.meshtastic.app -import androidx.appfunctions.AppFunctionConfiguration import kotlinx.coroutines.launch import org.koin.java.KoinJavaComponent.getKoin import org.meshtastic.app.ai.appfunctions.AppFunctionStateSync -import org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions -/** - * Google flavor Application subclass that configures App Functions. - * - * Registers a custom factory so the AppFunctions runtime can instantiate [MeshtasticAppFunctions] with its Koin-managed - * dependencies. - */ -class GoogleMeshUtilApplication : - MeshUtilApplication(), - AppFunctionConfiguration.Provider { +/** Google flavor Application subclass that starts the App Functions enabled-state sync. */ +class GoogleMeshUtilApplication : MeshUtilApplication() { override fun onCreate() { super.onCreate() + if (!isSupportedDevice) return // Start the AppFunctions enabled-state sync. Resolved here (after startKoin has bound // androidContext) rather than via createdAtStart so that Koin graphs built outside a // running app — verification tests, previews — stay lazily constructible. // Off-main: construction forces the AppFunctionsPrefs subgraph and fires AppSearch binder calls. applicationScope.launch { getKoin().get() } } - - override val appFunctionConfiguration: AppFunctionConfiguration - get() = - AppFunctionConfiguration.Builder() - .addEnclosingClassFactory(MeshtasticAppFunctions::class.java) { - getKoin().get() - } - .build() } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/GeminiNanoDocAssistant.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/GeminiNanoDocAssistant.kt index 5fefbba9cb..fbe6f6cb9c 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/ai/GeminiNanoDocAssistant.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/GeminiNanoDocAssistant.kt @@ -28,6 +28,7 @@ import com.google.firebase.ai.ai import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.content +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -149,8 +150,9 @@ class GeminiNanoDocAssistant( false } } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) throw e Logger.w(tag = TAG) { "isSupported() check failed: ${e.message}" } _modelStatus.value = ModelReadiness.Unavailable(e.message) false @@ -160,8 +162,9 @@ class GeminiNanoDocAssistant( try { ext.warmUp() Logger.i(tag = TAG) { "Model warmed up successfully" } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) throw e Logger.w(tag = TAG) { "Warmup failed (non-fatal): ${e.message}" } } } @@ -278,8 +281,9 @@ class GeminiNanoDocAssistant( ), ) return@flow // Success — exit retry loop + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) throw e lastError = e val isBusy = e.message?.contains("BUSY", ignoreCase = true) == true || @@ -392,12 +396,11 @@ class GeminiNanoDocAssistant( val paragraphs = plainText.split(Regex("\n{2,}")).map { it.trim() }.filter { it.length >= MIN_PARAGRAPH_LEN } // Score each paragraph by how many query terms it contains. - val scored = - paragraphs.map { paragraph -> - val lower = paragraph.lowercase() - val hits = queryTerms.count { term -> lower.contains(term) } - paragraph to hits - } + val scored = paragraphs.map { paragraph -> + val lower = paragraph.lowercase() + val hits = queryTerms.count { term -> lower.contains(term) } + paragraph to hits + } // Take paragraphs with hits first (sorted by hits desc), then fill with top paragraphs for context. val withHits = scored.filter { it.second > 0 }.sortedByDescending { it.second } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt index 09c533b91c..53f9ec2481 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt @@ -17,6 +17,7 @@ package org.meshtastic.app.ai.appfunctions import android.content.Context +import android.os.Build import androidx.appfunctions.AppFunctionException import androidx.appfunctions.AppFunctionManager import androidx.appfunctions.metadata.AppFunctionName @@ -49,7 +50,8 @@ class AppFunctionStateSync( private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) init { - observeAndSync() + // Only the API 36 platform service is declared, so below it nothing of ours is ever indexed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) observeAndSync() } private fun observeAndSync() { @@ -101,11 +103,14 @@ class AppFunctionStateSync( } else { AppFunctionManager.APP_FUNCTION_STATE_DISABLED } + // Until the system indexes a function (first launch, or just after an update) writing it throws + // IllegalArgumentException; the read-back drives the retry. try { manager.setAppFunctionEnabled(functionId, state) } catch (e: AppFunctionException) { - // Usually "not indexed yet" on first launch; the read-back drives the retry. Logger.d(e) { "AppFunction $functionId not writable yet" } + } catch (e: IllegalArgumentException) { + Logger.d(e) { "AppFunction $functionId not indexed yet" } } } if (attempt < MAX_SYNC_ATTEMPTS - 1) delay(RETRY_DELAY_MS) @@ -134,7 +139,8 @@ class AppFunctionStateSync( ): List> = if (actual == null) desired else desired.filter { (id, enabled) -> actual[id] != enabled } - private const val CLASS_PREFIX = "org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions#" + // Mirrors the generated MeshtasticAppFunctionService.FUNCTION_ID_* constants, which are API 36 only. + private const val CLASS_PREFIX = "org.meshtastic.app.ai.appfunctions.BaseMeshtasticAppFunctionService#" const val SEND_MESSAGE_ID = "${CLASS_PREFIX}sendMessage" const val GET_MESH_STATUS_ID = "${CLASS_PREFIX}getMeshStatus" diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt new file mode 100644 index 0000000000..7e44a3d309 --- /dev/null +++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.ai.appfunctions + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.appfunctions.AppFunction +import androidx.appfunctions.AppFunctionIntValueConstraint +import androidx.appfunctions.AppFunctionService +import androidx.appfunctions.AppFunctionServiceEntryPoint +import org.koin.android.ext.android.inject +import org.meshtastic.core.data.ai.AiFunctionProvider + +/** + * Exposes Meshtastic mesh networking capabilities to system AI assistants via the Android App Functions API. Functions + * declared here are discoverable by the system and can be invoked by AI agents such as Gemini. + * + * KSP generates the concrete [MeshtasticAppFunctionService] and its `meshtastic_app_function_service.xml` asset; the + * KDoc on each function is the description agents read. + */ +@RequiresApi(Build.VERSION_CODES.BAKLAVA) +@AppFunctionServiceEntryPoint( + serviceName = "MeshtasticAppFunctionService", + appFunctionXmlFileName = "meshtastic_app_function_service", +) +abstract class BaseMeshtasticAppFunctionService : AppFunctionService() { + + private val appFunctions: MeshtasticAppFunctions by inject() + + /** + * Send a text message over the Meshtastic mesh radio network. + * + * Messages are transmitted to nearby mesh nodes using LoRa radio. The mesh network is ideal for off-grid + * communications where cellular service is unavailable. + * + * @param text The message text to send (max 228 UTF-8 bytes — the mesh payload left after protobuf framing). + * @param recipientName Optional name of a specific node to send a direct message to. If omitted, the message is + * broadcast to all nodes on the specified channel. + * @param channelName Optional channel name to broadcast on. If omitted, uses the primary channel. Ignored when + * recipientName is specified. + * @return A [SendMessageResponse] with the message ID, channel, and timestamp. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun sendMessage( + text: String, + recipientName: String? = null, + channelName: String? = null, + ): SendMessageResponse = appFunctions.sendMessage(text, recipientName, channelName) + + /** + * Get the current status of the Meshtastic mesh network. + * + * Returns connection state, number of online nodes, total known nodes, the connected device's battery level, and + * the local node name. + * + * @return A [MeshStatusResponse] with the current mesh network status. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getMeshStatus(): MeshStatusResponse = appFunctions.getMeshStatus() + + /** + * List all nodes currently visible on the Meshtastic mesh network. + * + * Returns detailed information about each node including name, battery level, and last heard time. Nodes are sorted + * by most recently heard first. + * + * @return A list of nodes with their current status and metrics. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getNodeList(): GetNodeListResponse = appFunctions.getNodeList() + + /** + * List all available Meshtastic mesh channels and their configurations. + * + * Returns details about each channel including name, index, primary status, and uplink/downlink settings. + * + * @return A list of channels with their current configuration. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getChannelInfo(): GetChannelInfoResponse = appFunctions.getChannelInfo() + + /** + * Get the status and metrics of the local Meshtastic radio device. + * + * Returns hardware model, firmware version, battery level, charging status, and current radio state. + * + * @return Device status with current metrics and configuration. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getDeviceStatus(): GetDeviceStatusResponse = appFunctions.getDeviceStatus() + + /** + * Retrieve detailed telemetry and status for a specific mesh node. + * + * Returns per-node metrics including battery level, signal strength, hardware model, and location data. + * + * @param nodeId The target node ID (e.g., '!abc12345' or user ID). + * @return A [GetNodeDetailsResponse] with detailed node information. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getNodeDetails(nodeId: String): GetNodeDetailsResponse = appFunctions.getNodeDetails(nodeId) + + /** + * Retrieve aggregate network metrics and statistics for the entire mesh. + * + * Returns mesh-wide analytics including total node count, online nodes, average battery level, and health score. + * + * @return A [GetMeshMetricsResponse] with mesh-wide statistics. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getMeshMetrics(): GetMeshMetricsResponse = appFunctions.getMeshMetrics() + + /** + * Retrieve recent messages received over the Meshtastic mesh radio network. + * + * Returns a list of recent messages from the local message history. Messages are stored locally and do not require + * an active mesh connection. Useful for catching up on conversations or reviewing recent communications. + * + * @param contactName Optional name of a node or channel to filter messages from. If omitted, returns messages from + * all contacts sorted by most recent. + * @param limit Maximum number of messages to return: 1, 5, 10, 20 or 50. Defaults to 20. + * @return A [GetRecentMessagesResponse] containing the list of recent messages. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getRecentMessages( + contactName: String? = null, + @AppFunctionIntValueConstraint(enumValues = [1, 5, 10, 20, 50]) + limit: Int = AiFunctionProvider.DEFAULT_MESSAGE_LIMIT, + ): GetRecentMessagesResponse = appFunctions.getRecentMessages(contactName, limit) + + /** + * Get a summary of unread messages across all Meshtastic mesh contacts. + * + * Returns the total unread count and a per-contact breakdown showing who sent unread messages, how many are unread, + * and a preview of the last message. Muted contacts are excluded. Does not require an active mesh connection. + * + * @return A [GetUnreadSummaryResponse] with the total unread count and per-contact details. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun getUnreadSummary(): GetUnreadSummaryResponse = appFunctions.getUnreadSummary() +} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt index 46463302ba..0097575bbb 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt @@ -16,10 +16,7 @@ */ package org.meshtastic.app.ai.appfunctions -import androidx.appfunctions.AppFunction -import androidx.appfunctions.AppFunctionContext import androidx.appfunctions.AppFunctionElementNotFoundException -import androidx.appfunctions.AppFunctionIntValueConstraint import androidx.appfunctions.AppFunctionInvalidArgumentException import androidx.appfunctions.AppFunctionNotSupportedException import kotlinx.coroutines.TimeoutCancellationException @@ -27,35 +24,12 @@ import org.meshtastic.core.data.ai.AiFunctionProvider import org.meshtastic.core.data.ai.SendMessageResult /** - * Exposes Meshtastic mesh networking capabilities to system AI assistants via the Android App Functions API. Functions - * declared here are discoverable by the system and can be invoked by AI agents such as Gemini. + * Maps [AiFunctionProvider] results to App Functions responses and exceptions. [BaseMeshtasticAppFunctionService] + * declares the functions agents see and delegates each one here. */ -// The AppFunctions calling convention requires every @AppFunction to take AppFunctionContext as -// its first parameter, even when the implementation never reads it. -@Suppress("UnusedParameter") class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { - /** - * Send a text message over the Meshtastic mesh radio network. - * - * Messages are transmitted to nearby mesh nodes using LoRa radio. The mesh network is ideal for off-grid - * communications where cellular service is unavailable. - * - * @param context The app function invocation context provided by the system. - * @param text The message text to send (max 228 UTF-8 bytes — the mesh payload left after protobuf framing). - * @param recipientName Optional name of a specific node to send a direct message to. If omitted, the message is - * broadcast to all nodes on the specified channel. - * @param channelName Optional channel name to broadcast on. If omitted, uses the primary channel. Ignored when - * recipientName is specified. - * @return A [SendMessageResponse] with the message ID, channel, and timestamp. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun sendMessage( - context: AppFunctionContext, - text: String, - recipientName: String? = null, - channelName: String? = null, - ): SendMessageResponse { + suspend fun sendMessage(text: String, recipientName: String?, channelName: String?): SendMessageResponse { val result = try { provider.sendMessage(text, recipientName, channelName) @@ -91,17 +65,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Get the current status of the Meshtastic mesh network. - * - * Returns connection state, number of online nodes, total known nodes, the connected device's battery level, and - * the local node name. - * - * @param context The app function invocation context provided by the system. - * @return A [MeshStatusResponse] with the current mesh network status. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getMeshStatus(context: AppFunctionContext): MeshStatusResponse { + suspend fun getMeshStatus(): MeshStatusResponse { val status = try { provider.getMeshStatus() @@ -120,17 +84,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { ) } - /** - * List all nodes currently visible on the Meshtastic mesh network. - * - * Returns detailed information about each node including name, battery level, and last heard time. Nodes are sorted - * by most recently heard first. - * - * @param context The app function invocation context provided by the system. - * @return A list of nodes with their current status and metrics. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getNodeList(context: AppFunctionContext): GetNodeListResponse { + suspend fun getNodeList(): GetNodeListResponse { val result = try { provider.getNodeList() @@ -163,16 +117,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * List all available Meshtastic mesh channels and their configurations. - * - * Returns details about each channel including name, index, primary status, and uplink/downlink settings. - * - * @param context The app function invocation context provided by the system. - * @return A list of channels with their current configuration. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getChannelInfo(context: AppFunctionContext): GetChannelInfoResponse { + suspend fun getChannelInfo(): GetChannelInfoResponse { val result = try { provider.getChannelInfo() @@ -205,16 +150,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Get the status and metrics of the local Meshtastic radio device. - * - * Returns hardware model, firmware version, battery level, charging status, and current radio state. - * - * @param context The app function invocation context provided by the system. - * @return Device status with current metrics and configuration. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getDeviceStatus(context: AppFunctionContext): GetDeviceStatusResponse { + suspend fun getDeviceStatus(): GetDeviceStatusResponse { val result = try { provider.getDeviceStatus() @@ -243,17 +179,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Retrieve detailed telemetry and status for a specific mesh node. - * - * Returns per-node metrics including battery level, signal strength, hardware model, and location data. - * - * @param context The app function invocation context provided by the system. - * @param nodeId The target node ID (e.g., '!abc12345' or user ID). - * @return A [GetNodeDetailsResponse] with detailed node information. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getNodeDetails(context: AppFunctionContext, nodeId: String): GetNodeDetailsResponse { + suspend fun getNodeDetails(nodeId: String): GetNodeDetailsResponse { val result = try { provider.getNodeDetails(nodeId) @@ -294,16 +220,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Retrieve aggregate network metrics and statistics for the entire mesh. - * - * Returns mesh-wide analytics including total node count, online nodes, average battery level, and health score. - * - * @param context The app function invocation context provided by the system. - * @return A [GetMeshMetricsResponse] with mesh-wide statistics. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getMeshMetrics(context: AppFunctionContext): GetMeshMetricsResponse { + suspend fun getMeshMetrics(): GetMeshMetricsResponse { val result = try { provider.getMeshMetrics() @@ -332,25 +249,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Retrieve recent messages received over the Meshtastic mesh radio network. - * - * Returns a list of recent messages from the local message history. Messages are stored locally and do not require - * an active mesh connection. Useful for catching up on conversations or reviewing recent communications. - * - * @param context The app function invocation context provided by the system. - * @param contactName Optional name of a node or channel to filter messages from. If omitted, returns messages from - * all contacts sorted by most recent. - * @param limit Maximum number of messages to return (1–50). Defaults to 20. - * @return A [GetRecentMessagesResponse] containing the list of recent messages. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getRecentMessages( - context: AppFunctionContext, - contactName: String? = null, - @AppFunctionIntValueConstraint(enumValues = [1, 5, 10, 20, 50]) - limit: Int = AiFunctionProvider.DEFAULT_MESSAGE_LIMIT, - ): GetRecentMessagesResponse { + suspend fun getRecentMessages(contactName: String?, limit: Int): GetRecentMessagesResponse { val result = try { provider.getRecentMessages(contactName, limit) @@ -381,17 +280,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) { } } - /** - * Get a summary of unread messages across all Meshtastic mesh contacts. - * - * Returns the total unread count and a per-contact breakdown showing who sent unread messages, how many are unread, - * and a preview of the last message. Muted contacts are excluded. Does not require an active mesh connection. - * - * @param context The app function invocation context provided by the system. - * @return A [GetUnreadSummaryResponse] with the total unread count and per-contact details. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun getUnreadSummary(context: AppFunctionContext): GetUnreadSummaryResponse { + suspend fun getUnreadSummary(): GetUnreadSummaryResponse { val result = try { provider.getUnreadSummary() diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt index a96ba3a013..6efb4caaa8 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt @@ -16,10 +16,12 @@ */ package org.meshtastic.app.analytics +import android.app.ActivityManager import android.app.Application import android.content.Context import android.os.Build import android.os.Bundle +import android.os.SystemClock import android.provider.Settings import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope @@ -57,6 +59,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.koin.core.annotation.Single import org.meshtastic.app.BuildConfig +import org.meshtastic.core.common.log.ErrorReportThrottle import org.meshtastic.core.common.log.shouldDowngradeForDatadog import org.meshtastic.core.common.log.shouldReportAsException import org.meshtastic.core.repository.AnalyticsPrefs @@ -109,8 +112,15 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic private const val KEY_PRIORITY = "priority" private const val KEY_TAG = "tag" private const val KEY_MESSAGE = "message" + private const val KEY_SUPPRESSED = "suppressed_since_last_report" + private const val KEY_FIRMWARE_VERSION = "firmware_version" + private const val KEY_DEVICE_HARDWARE = "device_hardware" } + /** Separate budgets per backend — see the comment in [DatadogLogWriter]. */ + private val reportThrottle = ErrorReportThrottle(nowMs = SystemClock::elapsedRealtime) + private val datadogThrottle = ErrorReportThrottle(nowMs = SystemClock::elapsedRealtime) + init { // Setup Kermit log writers immediately, they will handle delayed SDK initialization gracefully. val writers = buildList { @@ -190,6 +200,14 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic .build() Rum.enable(rumConfiguration) + val activityManager = application.getSystemService(ActivityManager::class.java) + heapLimitAttributes( + memoryClassMb = activityManager?.memoryClass, + largeMemoryClassMb = activityManager?.largeMemoryClass, + maxMemoryBytes = Runtime.getRuntime().maxMemory(), + ) + .forEach { (key, value) -> GlobalRumMonitor.get().addAttribute(key, value) } + val logsConfig = LogsConfiguration.Builder().build() Logs.enable(logsConfig) @@ -272,8 +290,24 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic } override fun setDeviceAttributes(firmwareVersion: String, model: String) { + val semanticFirmware = firmwareVersion.extractSemanticVersion() + + // The connected radio is the most diagnostic axis this app has, and crash triage happens in Crashlytics. + // Without these a crash report says nothing about which hardware or firmware produced it. Keys are sticky + // for the process and are refreshed on every connect; there is no disconnect hook on PlatformAnalytics, so + // a crash after an explicit disconnect still carries the last radio's values. + // Deliberately not gated on isCrashlyticsCollectionEnabled: setting a key is legal while collection is off, + // and updateAnalyticsConsent does not replay device attributes, so gating would leave the keys unset until + // the next connect for anyone who grants consent after pairing a radio. + if (isFirebaseInitialized) { + Firebase.crashlytics.setCustomKeys { + key(KEY_FIRMWARE_VERSION, semanticFirmware) + key(KEY_DEVICE_HARDWARE, model) + } + } + if (!Datadog.isInitialized() || !GlobalRumMonitor.isRegistered()) return - GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion.extractSemanticVersion()) + GlobalRumMonitor.get().addAttribute("firmware_version", semanticFirmware) GlobalRumMonitor.get().addAttribute("device_hardware", model) } @@ -330,6 +364,12 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic // Cancellations and expected conditions stay breadcrumbs only — see shouldReportAsException. if (!shouldReportAsException(severity, throwable)) return + val suppressed = reportThrottle.acquire(ErrorReportThrottle.signature(tag, message)) ?: return + + // Custom keys stay set for every later report, so this is written unconditionally — skipping it when + // the count is zero would leave an earlier report's positive count attached to this one. + Firebase.crashlytics.setCustomKeys { key(KEY_SUPPRESSED, suppressed) } + if (throwable != null) { Firebase.crashlytics.recordException(throwable) } else { @@ -338,7 +378,7 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic key(KEY_TAG, tag) key(KEY_MESSAGE, message) } - Firebase.crashlytics.recordException(Exception(message)) + Firebase.crashlytics.recordException(loggedException(message)) } } } @@ -351,6 +391,15 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic // error tracking while still emitting the log line. Note this deliberately keeps CancellationException // at error here even though Crashlytics drops it; see shouldDowngradeForDatadog. val effectiveSeverity = if (shouldDowngradeForDatadog(severity, throwable)) Severity.Warn else severity + + // Only error-level logs become RUM errors, so only those are worth throttling; everything below stays a + // plain log line and is cheap. Sharing the throttle with the Crashlytics writer would halve each + // backend's allowance, so they keep separate budgets for the same signature. + var suppressed = 0 + if (effectiveSeverity >= Severity.Error) { + suppressed = datadogThrottle.acquire(ErrorReportThrottle.signature(tag, message)) ?: return + } + val datadogPriority = when (effectiveSeverity) { Severity.Verbose -> android.util.Log.VERBOSE @@ -360,7 +409,12 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic Severity.Error -> android.util.Log.ERROR Severity.Assert -> android.util.Log.ASSERT } - logger.log(datadogPriority, message, throwable, mapOf("tag" to tag)) + val attributes = + buildMap { + put("tag", tag) + if (suppressed > 0) put(KEY_SUPPRESSED, suppressed) + } + logger.log(datadogPriority, message, throwable, attributes) } } @@ -401,3 +455,31 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic Firebase.analytics.logEvent(event, bundle) } } + +/** + * Class-name prefixes of the logging machinery that sits between a call site and [loggedException]. + * + * Resolved at runtime rather than written as literals so they still match after R8 renames them. Kermit is matched by + * the package of [LogWriter], which is the Kermit type this file already depends on. + */ +private val loggingFramePrefixes: List = + listOfNotNull( + GooglePlatformAnalytics::class.java.name, + LogWriter::class.java.name.substringBeforeLast('.', "").takeIf { it.isNotBlank() }, + ) + +/** + * Builds the stand-in exception for a log line that carried no [Throwable]. + * + * Crashlytics groups a report by the topmost frame it can attribute. Because the exception is constructed inside the + * log writer, every such stack starts with that writer and Kermit's dispatch, and Crashlytics walks past synthetic + * coroutine lambda frames looking for something nameable — so every error logged from inside a `suspend` block + * collapsed into a single `BaseLogger.processLog` issue (4,636 events across 1,203 users in a day and a half, hiding a + * dozen unrelated signatures). Dropping the logging frames puts the real call site on top. + * + * Falls back to the untrimmed stack if the trim would leave nothing, since R8 may repackage these classes. + */ +private fun loggedException(message: String): Exception = Exception(message).apply { + val callerFrames = stackTrace.dropWhile { frame -> loggingFramePrefixes.any { frame.className.startsWith(it) } } + if (callerFrames.isNotEmpty()) stackTrace = callerFrames.toTypedArray() +} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/analytics/HeapLimitAttributes.kt b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/HeapLimitAttributes.kt new file mode 100644 index 0000000000..eb14e2ab4f --- /dev/null +++ b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/HeapLimitAttributes.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.analytics + +private const val BYTES_PER_MB = 1024L * 1024L + +/** + * RUM attributes for the heap ceilings the device gives this process, all in the MB `ActivityManager.memoryClass` uses. + * The memory classes are left out when `ActivityManager` is unavailable. + */ +internal fun heapLimitAttributes( + memoryClassMb: Int?, + largeMemoryClassMb: Int?, + maxMemoryBytes: Long, +): Map = buildMap { + memoryClassMb?.let { put("heap_memory_class_mb", it) } + largeMemoryClassMb?.let { put("heap_large_memory_class_mb", it) } + put("heap_max_memory_mb", maxMemoryBytes / BYTES_PER_MB) +} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt index 50395fc725..3c3bcf10af 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt @@ -21,6 +21,7 @@ import com.google.mlkit.genai.prompt.Generation import com.google.mlkit.genai.prompt.GenerativeModel import com.google.mlkit.genai.prompt.TextPart import com.google.mlkit.genai.prompt.generateContentRequest +import kotlinx.coroutines.CancellationException import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.feature.discovery.DiscoverySummaryGenerator @@ -94,6 +95,8 @@ class GeminiNanoSummaryProvider(private val generator: DiscoverySummaryGenerator } else { text } + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { log.w(e) { "Gemini Nano generation failed, using fallback" } fallback() diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt deleted file mode 100644 index e15d5b5499..0000000000 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.app.map - -import android.Manifest -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.pm.PackageManager -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import co.touchlab.kermit.Logger -import com.google.android.gms.common.api.ResolvableApiException -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.LocationSettingsRequest -import com.google.android.gms.location.Priority - -private const val INTERVAL_MILLIS = 10000L - -@Suppress("LongMethod") -@Composable -fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) { - val context = LocalContext.current - var localHasPermission by remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED, - ) - } - - val requestLocationPermissionLauncher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) { isGranted -> - localHasPermission = isGranted - // Defer to the LaunchedEffect(localHasPermission) to check settings before confirming via - // onPermissionResult - // if permission is granted. If not granted, immediately report false. - if (!isGranted) { - onPermissionResult(false) - } - } - - val locationSettingsLauncher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { - Logger.d { "Location settings changed by user." } - // User has enabled location services or improved accuracy. - onPermissionResult(true) // Settings are now adequate, and permission was already granted. - } else { - Logger.d { "Location settings change cancelled by user." } - // User chose not to change settings. The permission itself is still granted, - // but the experience might be degraded. For the purpose of enabling map features, - // we consider this as success if the core permission is there. - // If stricter handling is needed (e.g., block feature if settings not optimal), - // this logic might change. - onPermissionResult(localHasPermission) - } - } - - LaunchedEffect(Unit) { - // Initial permission check - when (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)) { - PackageManager.PERMISSION_GRANTED -> { - if (!localHasPermission) { - localHasPermission = true - } - // If permission is already granted, proceed to check location settings. - // The LaunchedEffect(localHasPermission) will handle this. - // No need to call onPermissionResult(true) here yet, let settings check complete. - } - - else -> { - // Request permission if not granted. The launcher's callback will update localHasPermission. - requestLocationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) - } - } - } - - LaunchedEffect(localHasPermission) { - // Handles logic after permission status is known/updated - if (localHasPermission) { - // Permission is granted, now check location settings - val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, INTERVAL_MILLIS).build() - - val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest) - - val client = LocationServices.getSettingsClient(context) - val task = client.checkLocationSettings(builder.build()) - - task.addOnSuccessListener { - Logger.d { "Location settings are satisfied." } - onPermissionResult(true) // Permission granted and settings are good - } - - task.addOnFailureListener { exception -> - if (exception is ResolvableApiException) { - try { - val intentSenderRequest = IntentSenderRequest.Builder(exception.resolution).build() - locationSettingsLauncher.launch(intentSenderRequest) - // Result of this launch will be handled by locationSettingsLauncher's callback - } catch (sendEx: ActivityNotFoundException) { - Logger.d { "Error launching location settings resolution ${sendEx.message}." } - onPermissionResult(true) // Permission is granted, but settings dialog failed. Proceed. - } - } else { - Logger.d { "Location settings are not satisfiable.${exception.message}" } - onPermissionResult(true) // Permission is granted, but settings not ideal. Proceed. - } - } - } else { - // If permission is not granted, report false. - // This case is primarily handled by the requestLocationPermissionLauncher's callback - // if the initial state was denied, or if user denies it. - onPermissionResult(false) - } - } -} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt index 007ff47b37..e16b2594d4 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -22,6 +22,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.graphics.Bitmap +import android.graphics.Paint import android.location.Location import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -42,7 +43,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -59,8 +59,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.createBitmap import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import com.google.android.gms.location.FusedLocationProviderClient @@ -70,6 +79,7 @@ import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.GroundOverlay @@ -77,6 +87,8 @@ import com.google.android.gms.maps.model.GroundOverlayOptions import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds +import com.google.android.gms.maps.model.StrokeStyle +import com.google.android.gms.maps.model.StyleSpan import com.google.maps.android.SphericalUtil import com.google.maps.android.compose.CameraPositionState import com.google.maps.android.compose.Circle @@ -88,7 +100,7 @@ import com.google.maps.android.compose.MapType import com.google.maps.android.compose.MapUiSettings import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.MarkerComposable -import com.google.maps.android.compose.MarkerInfoWindowComposable +import com.google.maps.android.compose.MarkerInfoWindow import com.google.maps.android.compose.Polygon import com.google.maps.android.compose.Polyline import com.google.maps.android.compose.TileOverlay @@ -109,9 +121,11 @@ import com.google.maps.android.data.renderer.model.PointStyle import com.google.maps.android.data.renderer.model.PolygonStyle import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -130,14 +144,18 @@ import org.meshtastic.app.map.offline.terrain.ContourOverlay import org.meshtastic.app.map.offline.terrain.HillshadeTileProvider import org.meshtastic.app.map.tiles.RasterBasemap import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.common.util.NumberFormatter +import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.Node import org.meshtastic.core.model.TracerouteOverlay import org.meshtastic.core.model.geofence.toGeofence +import org.meshtastic.core.model.hasFix import org.meshtastic.core.model.isLocked import org.meshtastic.core.model.isModifiableBy import org.meshtastic.core.model.util.GeoConstants.DEG_D import org.meshtastic.core.model.util.GeoConstants.HEADING_DEG +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.model.util.isValidCodePoint import org.meshtastic.core.model.util.kmhIn import org.meshtastic.core.model.util.metersIn @@ -167,7 +185,6 @@ import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.icon.Layers import org.meshtastic.core.ui.icon.Map import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.TripOrigin import org.meshtastic.core.ui.theme.TracerouteColors import org.meshtastic.core.ui.util.ActiveWhileStarted import org.meshtastic.core.ui.util.KeepScreenOn @@ -175,9 +192,11 @@ import org.meshtastic.core.ui.util.PermissionStatus import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.formatPositionTime import org.meshtastic.core.ui.util.rememberLocationPermissionState +import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState import org.meshtastic.feature.map.MapBounds import org.meshtastic.feature.map.MapNodePolicy +import org.meshtastic.feature.map.TRACK_STOP_RADIUS_METERS import org.meshtastic.feature.map.component.ClusterMemberEntry import org.meshtastic.feature.map.component.ClusterMembersDialog import org.meshtastic.feature.map.component.CustomMapLayersSheet @@ -186,6 +205,7 @@ import org.meshtastic.feature.map.component.EditWaypointDialog import org.meshtastic.feature.map.component.MapButton import org.meshtastic.feature.map.component.MapControlsOverlay import org.meshtastic.feature.map.component.MapFilterSheet +import org.meshtastic.feature.map.component.MeshMapFitPadding import org.meshtastic.feature.map.component.NodeTrackFilterMenu import org.meshtastic.feature.map.component.OfflineStatusBanner import org.meshtastic.feature.map.component.RasterOverlayToggles @@ -201,6 +221,7 @@ import org.meshtastic.feature.map.layers.LayerType import org.meshtastic.feature.map.layers.MapLayerItem import org.meshtastic.feature.map.layers.opacityOf import org.meshtastic.feature.map.layers.toPickedMapFile +import org.meshtastic.feature.map.mergeStationaryRuns import org.meshtastic.feature.map.terrain.MapterhornEndpoints import org.meshtastic.feature.map.tiles.mapAttributionText import org.meshtastic.feature.map.tracerouteNodeSelection @@ -250,6 +271,26 @@ sealed interface GoogleMapMode { private const val TRACEROUTE_OFFSET_METERS = 100.0 private const val TRACEROUTE_BOUNDS_PADDING_PX = 120 +/** + * Fits [bounds] inside [MeshMapFitPadding], clear of the toolbar and the zoom pair. `newLatLngBounds` takes one padding + * for every edge and centres the fit, so it fits the padded box and then moves that box to where the padding puts it. + */ +private fun CameraPositionState.frameInsideChrome( + bounds: LatLngBounds, + mapSize: IntSize, + density: Density, + layoutDirection: LayoutDirection, +) = with(density) { + val left = MeshMapFitPadding.calculateLeftPadding(layoutDirection).roundToPx() + val right = MeshMapFitPadding.calculateRightPadding(layoutDirection).roundToPx() + val top = MeshMapFitPadding.calculateTopPadding().roundToPx() + val bottom = MeshMapFitPadding.calculateBottomPadding().roundToPx() + val width = (mapSize.width - left - right).coerceAtLeast(1) + val height = (mapSize.height - top - bottom).coerceAtLeast(1) + move(CameraUpdateFactory.newLatLngBounds(bounds, width, height, 0)) + move(CameraUpdateFactory.scrollBy((right - left) / 2f, (bottom - top) / 2f)) +} + // Shared geofence overlay styling (orange, matching the fdroid flavor). private val GEOFENCE_OVERLAY_COLOR = Color(0xFFFF9800) private const val GEOFENCE_FILL_ALPHA = 0.12f @@ -274,6 +315,16 @@ private const val TERRAIN_HILLSHADE_Z_INDEX = -0.9f private val ATTRIBUTION_BOTTOM_PADDING = 4.dp private const val ATTRIBUTION_SCRIM_ALPHA = 0.7f +/** Faded rather than transparent, so the oldest point stays visible and tappable, as on the MapLibre track. */ +private const val OLDEST_TRACK_ALPHA = 0.25f +private const val TRACK_FADE_LEVELS = 8 +private const val MAX_TRACK_MARKERS = 500 +private const val TRACK_POINT_SIZE_DP = 24f +private const val SELECTED_TRACK_POINT_SIZE_DP = 32f +private const val TRACK_POINT_OUTER_FRACTION = 10f / 24f +private const val TRACK_POINT_RING_FRACTION = 4f / 24f +private const val COORDINATE_DECIMALS = 5 + @Suppress("CyclomaticComplexMethod", "LongMethod") @OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class) @Composable @@ -287,6 +338,9 @@ fun MapView( val coroutineScope = rememberCoroutineScope() val mapLayers by mapViewModel.mapLayers.collectAsStateWithLifecycle() + // Collected here, not in a sheet: basemap selection and network layers report errors while no sheet is open. + LaunchedEffect(mapViewModel) { mapViewModel.errorFlow.collectLatest { context.showToast(it) } } + // --- Location permissions --- val locationPermission = rememberLocationPermissionState() var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) } @@ -383,6 +437,7 @@ fun MapView( try { cameraPositionState.animate(cameraUpdate) } catch (e: IllegalStateException) { + if (e is CancellationException) currentCoroutineContext().ensureActive() Logger.d { "Error animating camera to location: ${e.message}" } } } @@ -419,7 +474,12 @@ fun MapView( val filteredNodes = MapNodePolicy.visibleNodes(allNodes, mapFilterState, nowSeconds, ourNodeInfo?.num) - LaunchedEffect(mode, cameraInitialization, isMapLoaded, filteredNodes) { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + var mapSize by remember { mutableStateOf(IntSize.Zero) } + LaunchedEffect(mode, cameraInitialization, isMapLoaded, filteredNodes, mapSize) { + // The fit is sized to the map, so it waits for the first layout. + if (mapSize == IntSize.Zero) return@LaunchedEffect if ( mode is GoogleMapMode.Main && cameraInitialization == CameraInitialization.FitNodes && @@ -427,17 +487,14 @@ fun MapView( filteredNodes.isNotEmpty() ) { val points = filteredNodes.map { it.position.toLatLng() } - val cameraUpdate = - if (points.size == 1) { - CameraUpdateFactory.newLatLngZoom(points.first(), 12f) - } else { - // Shared with the MapLibre map, which pads a degenerate box rather than handing the camera - // something it cannot fit to. - val bounds = MapBounds.aroundNodes(filteredNodes)?.toLatLngBounds() - if (bounds == null) return@LaunchedEffect - CameraUpdateFactory.newLatLngBounds(bounds, 80) - } - cameraPositionState.move(cameraUpdate) + if (points.size == 1) { + cameraPositionState.move(CameraUpdateFactory.newLatLngZoom(points.first(), 12f)) + } else { + // Shared with the MapLibre map, which pads a degenerate box rather than handing the camera + // something it cannot fit to. + val bounds = MapBounds.aroundNodes(filteredNodes)?.toLatLngBounds() ?: return@LaunchedEffect + cameraPositionState.frameInsideChrome(bounds, mapSize, density, layoutDirection) + } mapViewModel.onInitialNodeBoundsApplied() } } @@ -462,12 +519,14 @@ fun MapView( val mapColorScheme = if (dark) ComposeMapColorScheme.DARK else ComposeMapColorScheme.LIGHT // --- Mode-specific data --- - // Node track: apply time filter + // Node track: apply time filter, and drop reports with no fix so toLatLng never draws them at 0,0 val sortedTrackPositions = if (mode is GoogleMapMode.NodeTrack) { val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter remember(mode.positions, lastHeardTrackFilter) { - mode.positions.filter { lastHeardTrackFilter.includes(it.time, nowSeconds) }.sortedBy { it.time } + mode.positions + .filter { it.hasFix() && lastHeardTrackFilter.includes(it.time, nowSeconds) } + .sortedBy { it.time } } } else { emptyList() @@ -554,6 +613,8 @@ fun MapView( cameraPositionState.animate(cameraUpdate) hasCentered = true } catch (e: IllegalStateException) { + if (e is CancellationException) currentCoroutineContext().ensureActive() + // Reached for a user gesture interrupting animate() too: that cancels the animation, not this effect. Logger.d { "Error centering track map: ${e.message}" } } } @@ -565,6 +626,7 @@ fun MapView( try { cameraPositionState.animate(CameraUpdateFactory.newLatLng(selectedPos.toLatLng())) } catch (e: IllegalStateException) { + if (e is CancellationException) currentCoroutineContext().ensureActive() Logger.d { "Error animating to selected position: ${e.message}" } } } @@ -590,6 +652,7 @@ fun MapView( cameraPositionState.animate(cameraUpdate) hasCentered = true } catch (e: IllegalStateException) { + if (e is CancellationException) currentCoroutineContext().ensureActive() Logger.d { "Error centering traceroute overlay: ${e.message}" } } } @@ -625,7 +688,7 @@ fun MapView( Box(modifier = modifier) { GoogleMap( mapColorScheme = mapColorScheme, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().onSizeChanged { mapSize = it }, cameraPositionState = cameraPositionState, uiSettings = MapUiSettings( @@ -643,7 +706,11 @@ fun MapView( mapType = effectiveGoogleMapType, isMyLocationEnabled = isLocationTrackingEnabled && locationPermission.isGranted, ), - onMapLoaded = { isMapLoaded = true }, + onMapLoaded = { + isMapLoaded = true + // The store-screenshot capture waits for this tag instead of a fixed delay. + Logger.withTag("MapDrawn").d { "tiles drawn" } + }, onMapClick = { latLng -> if (isMainMode && boxAuthoringDraft != null) { val first = boxAuthoringFirstCorner @@ -1074,6 +1141,7 @@ fun MapView( cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition)) Logger.d { "Oriented map to north" } } catch (e: IllegalStateException) { + if (e is CancellationException) currentCoroutineContext().ensureActive() Logger.d { "Error orienting map to north: ${e.message}" } } } @@ -1203,21 +1271,19 @@ fun MapView( } } -private const val SECONDS_PER_MINUTE = 60L -private const val MILLIS_PER_SECOND = 1_000L - @Composable private fun rememberRelativeTimeBucket(): Long { val buckets = remember { relativeTimeBuckets() } - return buckets.collectAsStateWithLifecycle(initialValue = nowSeconds / SECONDS_PER_MINUTE).value + return buckets.collectAsStateWithLifecycle(initialValue = nowSeconds / TimeConstants.SECONDS_PER_MINUTE).value } internal fun relativeTimeBuckets(now: () -> Long = { nowSeconds }): Flow = flow { while (true) { val currentSeconds = now() - emit(currentSeconds / SECONDS_PER_MINUTE) - val secondsUntilNextMinute = SECONDS_PER_MINUTE - currentSeconds.mod(SECONDS_PER_MINUTE) - delay(secondsUntilNextMinute * MILLIS_PER_SECOND) + emit(currentSeconds / TimeConstants.SECONDS_PER_MINUTE) + val secondsUntilNextMinute = + TimeConstants.SECONDS_PER_MINUTE - currentSeconds.mod(TimeConstants.SECONDS_PER_MINUTE) + delay(secondsUntilNextMinute * TimeConstants.MS_PER_SEC) } } @@ -1358,12 +1424,13 @@ private fun WaypointGeofenceOverlay(waypoint: Waypoint) { // region --- Node Track Overlay --- /** - * Renders the position track polyline segments and markers inside a [GoogleMap] content scope. Each marker fades from - * transparent (oldest) to opaque (newest). The newest position shows the node's [NodeChip]; older positions show a - * [TripOrigin] dot with an info-window on tap. + * Renders the position track polyline and markers inside a [GoogleMap] content scope. Consecutive fixes within + * [TRACK_STOP_RADIUS_METERS] of each other draw as one point, the newest fix of their run. Markers fade from faint + * (oldest) to opaque (newest). The newest point shows the node's [NodeChip]; older points show a ring with an + * info-window on tap. Beyond the newest [MAX_TRACK_MARKERS] points the track is drawn by the line alone. * - * When [selectedPositionTime] matches a marker's `Position.time`, that marker is highlighted with the primary color and - * elevated z-index. Tapping a marker invokes [onPositionSelect] for list synchronization. + * When [selectedPositionTime] falls within a point's run, that marker is highlighted with the primary color and + * elevated z-index. Tapping a marker invokes [onPositionSelect] with its fix's time for list synchronization. */ @OptIn(MapsComposeExperimentalApi::class) @Composable @@ -1378,26 +1445,35 @@ private fun NodeTrackOverlay( ) { val isHighPriority = focusedNode.num == myNodeNum || focusedNode.isFavorite val activeNodeZIndex = if (isHighPriority) 5f else 4f + val trackColor = Color(focusedNode.colors.second) val selectedColor = MaterialTheme.colorScheme.primary + val density = LocalDensity.current.density - sortedPositions.forEachIndexed { index, position -> - key(position.time) { + // Every point shares one of a few prebuilt icons. A composable rendered to a bitmap per point runs on the main + // thread, and a track holds up to DEFAULT_MAX_LOGS points. + val pointIcons = + remember(trackColor, density) { + List(TRACK_FADE_LEVELS) { level -> + trackPointIcon(trackColor.copy(alpha = trackFadeAlpha(level)), TRACK_POINT_SIZE_DP, density) + } + } + val selectedIcon = + remember(selectedColor, density) { trackPointIcon(selectedColor, SELECTED_TRACK_POINT_SIZE_DP, density) } + val pointTitle = stringResource(Res.string.position) + val pointDescription = stringResource(Res.string.track_point) + val runs = remember(sortedPositions) { mergeStationaryRuns(sortedPositions) } + // Every marker is added on the main thread, so only the newest points and the selected one get one. + val firstMarkerIndex = (runs.size - MAX_TRACK_MARKERS).coerceAtLeast(0) + + runs.forEachIndexed { index, run -> + val position = run.position + val isSelected = selectedPositionTime?.let(run::covers) == true + if (index < firstMarkerIndex && !isSelected) return@forEachIndexed + // Keyed on the run's start: its newest fix changes while the node stays put. + key(run.firstTime) { val markerState = rememberUpdatedMarkerState(position = position.toLatLng()) - val alpha = - if (sortedPositions.size > 1) { - index.toFloat() / (sortedPositions.size.toFloat() - 1) - } else { - 1f - } - val isSelected = position.time == selectedPositionTime - val color = - if (isSelected) { - selectedColor - } else { - Color(focusedNode.colors.second).copy(alpha = alpha) - } - if (index == sortedPositions.lastIndex) { + if (index == runs.lastIndex) { MarkerComposable( state = markerState, zIndex = activeNodeZIndex, @@ -1410,44 +1486,62 @@ private fun NodeTrackOverlay( NodeChip(node = focusedNode) } } else { - MarkerInfoWindowComposable( + val level = trackFadeLevel(index, runs.lastIndex) + MarkerInfoWindow( state = markerState, - title = stringResource(Res.string.position), + contentDescription = pointDescription, + icon = if (isSelected) selectedIcon else pointIcons[level], + title = pointTitle, snippet = formatAgo(position.time), - zIndex = if (isSelected) activeNodeZIndex - 0.5f else 1f + alpha, + zIndex = if (isSelected) activeNodeZIndex - 0.5f else 1f + trackFadeAlpha(level), onClick = { onPositionSelect?.invoke(position.time) false // Allow default info window behavior }, - infoContent = { PositionInfoWindowContent(position = position, displayUnits = displayUnits) }, ) { - Icon( - imageVector = MeshtasticIcons.TripOrigin, - contentDescription = stringResource(Res.string.track_point), - tint = color, - modifier = if (isSelected) Modifier.size(32.dp) else Modifier, - ) + PositionInfoWindowContent(position = position, displayUnits = displayUnits) } } } } - // Gradient polyline segments - if (sortedPositions.size > 1) { - val segments = sortedPositions.windowed(size = 2, step = 1, partialWindows = false) - segments.forEachIndexed { index, segmentPoints -> - val alpha = index.toFloat() / (segments.size.toFloat() - 1) - Polyline( - points = segmentPoints.map { it.toLatLng() }, - jointType = JointType.ROUND, - color = Color(focusedNode.colors.second).copy(alpha = alpha), - width = 8f, - zIndex = 0.6f, - ) - } + if (runs.size > 1) { + val points = remember(runs) { runs.map { it.position.toLatLng() } } + // A span without a segment count covers only the first segment. + val spans = + remember(trackColor, points.size) { + val oldest = trackColor.copy(alpha = OLDEST_TRACK_ALPHA).toArgb() + val gradient = StrokeStyle.gradientBuilder(oldest, trackColor.toArgb()).build() + listOf(StyleSpan(gradient, (points.size - 1).toDouble())) + } + Polyline(points = points, spans = spans, jointType = JointType.ROUND, width = 8f, zIndex = 0.6f) } } +/** Buckets a point's position along the track so that appending a point changes the icon of only a few markers. */ +private fun trackFadeLevel(index: Int, lastIndex: Int): Int = + if (lastIndex <= 0) TRACK_FADE_LEVELS - 1 else index * (TRACK_FADE_LEVELS - 1) / lastIndex + +private fun trackFadeAlpha(level: Int): Float = + OLDEST_TRACK_ALPHA + (1f - OLDEST_TRACK_ALPHA) * level / (TRACK_FADE_LEVELS - 1) + +/** A ring, drawn straight to a bitmap so building it never composes. */ +private fun trackPointIcon(color: Color, sizeDp: Float, density: Float): BitmapDescriptor { + val sizePx = (sizeDp * density).roundToInt().coerceAtLeast(1) + val bitmap = createBitmap(sizePx, sizePx) + val stroke = sizePx * TRACK_POINT_RING_FRACTION + val paint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = stroke + this.color = color.toArgb() + } + // The ring's outer edge sits inside a 24-unit box at radius 10, matching the TripOrigin glyph. + val outerRadius = sizePx * TRACK_POINT_OUTER_FRACTION + bitmap.applyCanvas { drawCircle(sizePx / 2f, sizePx / 2f, outerRadius - stroke / 2f, paint) } + return BitmapDescriptorFactory.fromBitmap(bitmap) +} + @Composable @Suppress("LongMethod") private fun PositionInfoWindowContent(position: Position, displayUnits: MeasurementSystem = MeasurementSystem.METRIC) { @@ -1464,11 +1558,11 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: Measurem Column(modifier = Modifier.padding(8.dp)) { PositionRow( label = stringResource(Res.string.latitude), - value = "%.5f".format((position.latitude_i ?: 0) * DEG_D), + value = NumberFormatter.format((position.latitude_i ?: 0) * DEG_D, COORDINATE_DECIMALS), ) PositionRow( label = stringResource(Res.string.longitude), - value = "%.5f".format((position.longitude_i ?: 0) * DEG_D), + value = NumberFormatter.format((position.longitude_i ?: 0) * DEG_D, COORDINATE_DECIMALS), ) PositionRow(label = stringResource(Res.string.sats), value = position.sats_in_view.toString()) PositionRow( @@ -1478,7 +1572,7 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: Measurem PositionRow(label = stringResource(Res.string.speed), value = speedFromPosition(position, displayUnits)) PositionRow( label = stringResource(Res.string.heading), - value = "%.0f°".format((position.ground_track ?: 0) * HEADING_DEG), + value = "${NumberFormatter.format((position.ground_track ?: 0) * HEADING_DEG, 0)}°", ) PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime()) } @@ -1554,20 +1648,19 @@ private fun offsetPolyline( val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points - val headings = - headingPoints.mapIndexed { index, _ -> - when (index) { - 0 -> SphericalUtil.computeHeading(headingPoints[0], headingPoints[1]) + val headings = headingPoints.mapIndexed { index, _ -> + when (index) { + 0 -> SphericalUtil.computeHeading(headingPoints[0], headingPoints[1]) - headingPoints.lastIndex -> - SphericalUtil.computeHeading( - headingPoints[headingPoints.lastIndex - 1], - headingPoints[headingPoints.lastIndex], - ) + headingPoints.lastIndex -> + SphericalUtil.computeHeading( + headingPoints[headingPoints.lastIndex - 1], + headingPoints[headingPoints.lastIndex], + ) - else -> SphericalUtil.computeHeading(headingPoints[index - 1], headingPoints[index + 1]) - } + else -> SphericalUtil.computeHeading(headingPoints[index - 1], headingPoints[index + 1]) } + } return points.mapIndexed { index, point -> val heading = headings[index.coerceIn(0, headings.lastIndex)] @@ -1594,7 +1687,7 @@ private fun MapLayerOverlay(layerItem: MapLayerItem, opacity: Float, mapViewMode val layer = try { val dataLayer = - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { // Buffered because the KMZ sniff marks and resets the stream before the parser reads it. BufferedInputStream(ByteArrayInputStream(bytes)).use { stream -> parseMapLayer(layerItem.layerType, stream) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt index 0281835ec9..e0efb12d11 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt @@ -61,7 +61,6 @@ import org.meshtastic.app.map.tiles.RasterTileProvider import org.meshtastic.app.map.tiles.toRasterBasemap import org.meshtastic.core.common.util.LocaleUnitsProvider import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.MapPrefs @@ -72,6 +71,9 @@ import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioController import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getStringSuspend +import org.meshtastic.core.resources.url_http_localhost_only import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.BaseMapViewModel import org.meshtastic.feature.map.layers.LayerOpacityStore @@ -89,6 +91,8 @@ import org.meshtastic.feature.map.tiles.CustomTileProviderSaveResult import org.meshtastic.feature.map.tiles.MapTileCatalogue import org.meshtastic.feature.map.tiles.RasterOverlaySource import org.meshtastic.feature.map.tiles.RasterTileSpec +import org.meshtastic.feature.map.tiles.isCleartextPermitted +import org.meshtastic.feature.map.tiles.isRefusedCleartextTileUrl import org.meshtastic.feature.map.tiles.isValidTileUrlTemplate import java.io.File import java.io.FileOutputStream @@ -142,27 +146,13 @@ class MapViewModel( private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get("waypointId")) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() - // Injected by the map provider because this SavedStateHandle is not the Navigation 3 entry's route state. - private val sitePlannerRequestState = SitePlannerRequestState(nodeRepository.nodeDBbyNum) - val sitePlannerRequest: StateFlow = - sitePlannerRequestState.request.stateInWhileSubscribed(initialValue = null) - - fun setSitePlannerNodeNum(nodeNum: Int?) { - sitePlannerRequestState.setNodeNum(nodeNum) - } - - fun consumeSitePlannerRequest(nodeNum: Int) { - sitePlannerRequestState.consume(nodeNum) - } - fun setWaypointId(id: Int?) { if (_selectedWaypointId.value != id) { _selectedWaypointId.value = id if (id != null) { viewModelScope.launch { val wpMap = waypoints.first { it.containsKey(id) } - wpMap[id]?.let { packet -> - val waypoint = packet.waypoint!! + wpMap[id]?.waypoint?.let { waypoint -> val latLng = LatLng( (waypoint.latitude_i ?: 0) / WAYPOINT_COORD_SCALE, @@ -345,6 +335,9 @@ class MapViewModel( if (config != null) { if (!config.isLocal && !isValidTileUrlTemplate(config.urlTemplate)) { Logger.withTag("MapViewModel").w("Attempted to select an invalid custom tile URL template") + if (config.urlTemplate.isRefusedCleartextTileUrl()) { + viewModelScope.launch { _errorFlow.emit(getStringSuspend(Res.string.url_http_localhost_only)) } + } clearCurrentTileProvider() _selectedRasterBasemapId.value = null _selectedGoogleMapType.value = MapType.NORMAL @@ -443,8 +436,7 @@ class MapViewModel( } } - private fun isValidTileUrlTemplate(urlTemplate: String): Boolean = - urlTemplate.isValidTileUrlTemplate(requireHttps = false) + private fun isValidTileUrlTemplate(urlTemplate: String): Boolean = urlTemplate.isValidTileUrlTemplate() /** What to restore once the network returns from an auto-switch; null when nothing has been auto-switched. */ private data class OfflineAutoSwitchState(val rasterBasemapId: String?, val googleMapType: MapType) @@ -630,23 +622,22 @@ class MapViewModel( _terrainDownloadRegionId.value = regionId _terrainDownloadState.value = null - terrainDownloadJob = - viewModelScope.launch { - val store = terrainStoreForRegion(regionId) - val bounds = - GeoBounds( - south = region.southLat, - west = region.westLon, - north = region.northLat, - east = region.eastLon, - ) - val maxZoom = TerrainDownloadPlanner.maxZoomFitting(bounds, TerrainRegionExtractor.MAX_TILES) - // flowOn: the extractor does blocking per-tile HTTP on its collector's dispatcher. - TerrainRegionExtractor(store).download(bounds, maxZoom).flowOn(dispatchers.io).collect { state -> - _terrainDownloadState.value = state - if (state is TerrainDownloadState.Complete) attachTerrain(region, state, store) - } + terrainDownloadJob = viewModelScope.launch { + val store = terrainStoreForRegion(regionId) + val bounds = + GeoBounds( + south = region.southLat, + west = region.westLon, + north = region.northLat, + east = region.eastLon, + ) + val maxZoom = TerrainDownloadPlanner.maxZoomFitting(bounds, TerrainRegionExtractor.MAX_TILES) + // flowOn: the extractor does blocking per-tile HTTP on its collector's dispatcher. + TerrainRegionExtractor(store).download(bounds, maxZoom).flowOn(dispatchers.io).collect { state -> + _terrainDownloadState.value = state + if (state is TerrainDownloadState.Complete) attachTerrain(region, state, store) } + } } private suspend fun attachTerrain( @@ -711,8 +702,7 @@ class MapViewModel( selectedWaypointId.value?.let { wpId -> viewModelScope.launch { val wpMap = waypoints.first { it.containsKey(wpId) } - wpMap[wpId]?.let { packet -> - val waypoint = packet.waypoint!! + wpMap[wpId]?.waypoint?.let { waypoint -> val latLng = LatLng( (waypoint.latitude_i ?: 0) / WAYPOINT_COORD_SCALE, @@ -765,6 +755,7 @@ class MapViewModel( if (selection.customTileUrl != null) googleMapsPrefs.setSelectedCustomTileUrl(null) } else { _selectedRasterBasemapId.value = null + if (resolvedSelection.refusedCleartextSource) reportRefusedCleartextSource() if (resolvedSelection.canDiscardMissingSelection) { if (selectedProviderId != null) mapTileProviderPrefs.setSelectedCustomTileProviderId(null) if (selection.customTileUrl != null) googleMapsPrefs.setSelectedCustomTileUrl(null) @@ -783,6 +774,14 @@ class MapViewModel( } } + /** Waits for a collector: this runs from init, before the map collects, and the selection is cleared next. */ + private fun reportRefusedCleartextSource() { + viewModelScope.launch { + _errorFlow.subscriptionCount.first { it > 0 } + _errorFlow.emit(getStringSuspend(Res.string.url_http_localhost_only)) + } + } + fun addMapLayer(picked: PickedMapFile) = mapLayersManager.addMapLayer(picked) fun addNetworkMapLayer(name: String, url: String) { @@ -853,24 +852,30 @@ internal fun List.findLegacyCustomTileProvider( internal data class PersistedCustomTileSelection( val provider: CustomTileProviderConfig?, val canDiscardMissingSelection: Boolean, + val refusedCleartextSource: Boolean = false, ) internal fun List.resolvePersistedCustomTileSelection( selectedProviderId: String?, legacySource: String?, providerLoadSuccessful: Boolean, + cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted, ): PersistedCustomTileSelection { - val provider = + val candidates = listOfNotNull(findSelectedCustomTileProvider(selectedProviderId), findLegacyCustomTileProvider(legacySource)) - .firstOrNull { it.hasValidGoogleTileSource() } + val provider = candidates.firstOrNull { it.hasValidGoogleTileSource(cleartextPermitted) } return PersistedCustomTileSelection( provider = provider, canDiscardMissingSelection = provider == null && providerLoadSuccessful, + refusedCleartextSource = + provider == null && + candidates.any { !it.isLocal && it.urlTemplate.isRefusedCleartextTileUrl(cleartextPermitted) }, ) } -internal fun CustomTileProviderConfig.hasValidGoogleTileSource(): Boolean = - isLocal || urlTemplate.isValidTileUrlTemplate(requireHttps = false) +internal fun CustomTileProviderConfig.hasValidGoogleTileSource( + cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted, +): Boolean = isLocal || urlTemplate.isValidTileUrlTemplate(cleartextPermitted) private fun GoogleCameraPosition.toCameraPosition() = CameraPosition(LatLng(targetLat, targetLng), zoom, tilt, bearing) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt index d5c8162b6c..d2ba844352 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt @@ -22,16 +22,13 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.meshtastic.app.map.MapViewModel import org.meshtastic.app.map.importMbTiles -import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.component.CustomTileProviderManager import org.meshtastic.feature.map.layers.getFileName import java.io.File @@ -65,8 +62,6 @@ fun CustomTileProviderManagerSheet(mapViewModel: MapViewModel) { } } - LaunchedEffect(Unit) { mapViewModel.errorFlow.collectLatest { context.showToast(it) } } - CustomTileProviderManager( providers = providers, onAdd = mapViewModel::addCustomTileProvider, diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt index 0bb8cb9118..1d9b091da2 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt @@ -25,8 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.MapsComposeExperimentalApi -import com.google.maps.android.compose.Marker -import com.google.maps.android.compose.rememberComposeBitmapDescriptor +import com.google.maps.android.compose.MarkerComposable import com.google.maps.android.compose.rememberUpdatedMarkerState import org.jetbrains.compose.resources.stringResource import org.meshtastic.app.map.convertIntToEmoji @@ -67,10 +66,6 @@ fun WaypointMarkers( val iconCodePoint = waypoint.icon.waypointIconOrDefault() val emojiText = convertIntToEmoji(iconCodePoint) - val icon = - rememberComposeBitmapDescriptor(iconCodePoint) { - Text(text = emojiText, fontSize = 32.sp, modifier = Modifier.padding(2.dp)) - } // Non-visual cue: the geofence is otherwise only an orange overlay, so surface it in the marker's // accessible snippet for screen-reader and color-challenged users. @@ -88,9 +83,9 @@ fun WaypointMarkers( val cleanName = waypoint.name.replace('\n', ' ').replace('\b', ' ') val title = if (waypoint.isLocked) "${convertIntToEmoji(LOCK)} $cleanName" else cleanName - Marker( + MarkerComposable( + iconCodePoint, state = markerState, - icon = icon, title = title, snippet = snippet, visible = true, @@ -106,7 +101,9 @@ fun WaypointMarkers( else -> onDeleteWaypointRequest(waypoint) } }, - ) + ) { + Text(text = emojiText, fontSize = 32.sp, modifier = Modifier.padding(2.dp)) + } } } } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryMap.kt index 9dff450534..bcb7336666 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryMap.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryMap.kt @@ -17,11 +17,13 @@ package org.meshtastic.app.map.discovery import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable import androidx.compose.ui.Modifier import org.meshtastic.core.ui.util.DiscoveryMapNode /** Flavor-unified entry point for the discovery map. Google Maps implementation. */ @Composable +@NonRestartableComposable fun DiscoveryMap( userLatitude: Double, userLongitude: Double, diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt deleted file mode 100644 index 19ce8cff75..0000000000 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.app.map.node - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.meshtastic.app.map.GoogleMapMode -import org.meshtastic.app.map.MapView -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.map.node.NodeMapViewModel - -@Composable -fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) { - val node by nodeMapViewModel.node.collectAsStateWithLifecycle() - val positions by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle() - - Scaffold( - topBar = { - MainAppBar( - title = node?.user?.long_name ?: "", - ourNode = null, - showNodeChip = false, - canNavigateUp = true, - onNavigateUp = onNavigateUp, - actions = {}, - onClickChip = {}, - ) - }, - ) { paddingValues -> - MapView( - modifier = Modifier.fillMaxSize().padding(paddingValues), - mode = GoogleMapMode.NodeTrack(focusedNode = node, positions = positions), - ) - } -} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt index 21b52b154a..034f316ac5 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt @@ -41,6 +41,8 @@ fun NodeTrackMap( modifier: Modifier = Modifier, selectedPositionTime: Int? = null, onPositionSelect: ((Int) -> Unit)? = null, + // Accepted for the shared seam and ignored: Google Maps draws its own attribution, in MapView. + @Suppress("UNUSED_PARAMETER") showAttribution: Boolean = true, ) { val vm = koinViewModel() vm.setDestNum(destNum) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionExtractor.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionExtractor.kt index 8f1d9787e4..6ba200afc6 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionExtractor.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionExtractor.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.meshtastic.core.common.util.ioDispatcher +import org.meshtastic.core.common.util.nowSeconds import java.io.IOException import java.util.zip.GZIPInputStream import kotlin.uuid.Uuid @@ -121,7 +122,7 @@ internal class OfflineRegionExtractor(private val store: OfflineRegionStore) { maxZoom = zoomRange.last, tileCount = tiles.size.toLong(), byteSize = archiveFile.length(), - createdAtEpochSeconds = System.currentTimeMillis() / MILLIS_PER_SECOND, + createdAtEpochSeconds = nowSeconds, ) .also { store.add(it) } } catch (e: IOException) { @@ -171,7 +172,6 @@ internal class OfflineRegionExtractor(private val store: OfflineRegionStore) { const val MAX_REGIONS = 10 const val MAX_TOTAL_BYTES = 300L * 1024 * 1024 private const val PROGRESS_STRIDE = 10 - private const val MILLIS_PER_SECOND = 1_000L /** Both the Protomaps build and the MVT layers it packages (OpenStreetMap) require attribution. */ const val ATTRIBUTION = "© OpenStreetMap contributors, © Protomaps" diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionTileSet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionTileSet.kt index 3ca50f1c34..1e86433c5b 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionTileSet.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/OfflineRegionTileSet.kt @@ -16,56 +16,29 @@ */ package org.meshtastic.app.map.offline.pmtiles -import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds +import org.meshtastic.feature.map.terrain.GeoBounds +import org.meshtastic.feature.map.terrain.TerrainTileMath -/** The standard slippy-map tile enumeration for a region, shared by the download estimate and the extractor. */ +/** + * The standard slippy-map tile enumeration for a region, shared by the download estimate and the extractor. + * [TerrainTileMath] does the enumeration, including the antimeridian split. + */ internal object OfflineRegionTileSet { /** Every tile in [bounds] across [zoomRange], each zoom level's tiles listed before the next deepens. */ - fun tiles(bounds: LatLngBounds, zoomRange: IntRange): List = - zoomRange.flatMap { zoom -> tilesAtZoom(bounds, zoom) } - - fun estimateTileCount(bounds: LatLngBounds, zoomRange: IntRange): Long = - zoomRange.sumOf { zoom -> tilesAtZoom(bounds, zoom).size.toLong() } - - private fun tilesAtZoom(bounds: LatLngBounds, zoom: Int): List { - // Northwest corner has the smaller tile-column and smallest tile-row (XYZ rows increase southward); - // southeast has the larger of both. - val northwestCorner = LatLng(bounds.northeast.latitude, bounds.southwest.longitude) - val southeastCorner = LatLng(bounds.southwest.latitude, bounds.northeast.longitude) - val northwest = WebMercatorTileMath.tileAt(zoom, northwestCorner) - val southeast = WebMercatorTileMath.tileAt(zoom, southeastCorner) - - val tiles = mutableListOf() - for (xRange in xRangesAt(zoom, northwest, southeast)) { - for (x in xRange) { - for (y in northwest.y..southeast.y) { - tiles += TileIndex(zoom, x, y) - } - } - } - return tiles + fun tiles(bounds: LatLngBounds, zoomRange: IntRange): List = zoomRange.flatMap { zoom -> + TerrainTileMath.tilesAt(zoom, bounds.toGeoBounds()) } - /** - * The largest valid tile-column/row index at [zoom] — `2^zoom - 1`, the same bound [WebMercatorTileMath.tileAt] - * clamps into. - */ - private fun maxTileIndex(zoom: Int): Int = (1 shl zoom) - 1 + fun estimateTileCount(bounds: LatLngBounds, zoomRange: IntRange): Long = zoomRange.sumOf { zoom -> + TerrainTileMath.tileCountAt(zoom, bounds.toGeoBounds()) + } - /** - * The x-index ranges [tilesAtZoom] must enumerate — normally a single `[northwest.x, southeast.x]` range, but split - * into two (`[northwest.x, maxX]` and `[0, southeast.x]`) when the bounds cross the antimeridian (e.g. west=170, - * east=-170 — real for Fiji, or Chukotka/Alaska): a plain `IntRange` with `start > end` is empty in Kotlin, which - * would otherwise silently enumerate zero tiles for a real, valid region. Same fix as - * [org.meshtastic.feature.map.terrain.TerrainTileMath.tilesAt]'s `xRangesAt`, ported here since this package keeps - * its own tile math rather than depending on that module. - */ - private fun xRangesAt(zoom: Int, northwest: TileIndex, southeast: TileIndex): List = - if (northwest.x <= southeast.x) { - listOf(northwest.x..southeast.x) - } else { - listOf(northwest.x..maxTileIndex(zoom), 0..southeast.x) - } + private fun LatLngBounds.toGeoBounds() = GeoBounds( + south = southwest.latitude, + west = southwest.longitude, + north = northeast.latitude, + east = northeast.longitude, + ) } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt index 424e15961a..4355b34553 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt @@ -14,8 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +@file:OptIn(ExperimentalSerializationApi::class) + package org.meshtastic.app.map.offline.pmtiles +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/WebMercatorTileMath.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/WebMercatorTileMath.kt index c9dbb6234a..e341158ba0 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/WebMercatorTileMath.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/WebMercatorTileMath.kt @@ -17,49 +17,23 @@ package org.meshtastic.app.map.offline.pmtiles import com.google.android.gms.maps.model.LatLng -import kotlin.math.PI -import kotlin.math.asinh -import kotlin.math.atan -import kotlin.math.pow -import kotlin.math.sinh -import kotlin.math.tan +import org.meshtastic.feature.map.terrain.TerrainTileMath -/** Standard XYZ/slippy-map Web Mercator conversions, shared by tile enumeration and MVT-local-coordinate placement. */ +/** [TerrainTileMath] in Google Maps [LatLng] terms, for MVT-local-coordinate placement. */ internal object WebMercatorTileMath { - /** The tile (at [zoom]) containing [latLng] — the same indexing GoogleMap, MapLibre and PMTiles all share. */ - fun tileAt(zoom: Int, latLng: LatLng): TileIndex { - val n = 2.0.pow(zoom) - val latRad = Math.toRadians(latLng.latitude.coerceIn(-MAX_LATITUDE, MAX_LATITUDE)) - val x = ((latLng.longitude + LON_RANGE_DEG) / FULL_LON_RANGE_DEG * n).toInt().coerceIn(0, (n - 1).toInt()) - val y = ((1.0 - asinh(tan(latRad)) / PI) / 2.0 * n).toInt().coerceIn(0, (n - 1).toInt()) - return TileIndex(zoom, x, y) - } - /** Places a feature's tile-local point (`0 until extent` on each axis) at its real-world [LatLng]. */ fun tileLocalToLatLng(tile: TileIndex, extent: Int, local: TileCoord): LatLng = tileFractionalToLatLng(tile.zoom, tile.x, tile.y, local.x.toDouble() / extent, local.y.toDouble() / extent) /** - * Places a fractional `[0,1]×[0,1]` tile-local point — [org.meshtastic.feature.map.terrain.ContourPoint]'s own - * convention — at its real-world [LatLng]. A standalone zoom/x/y overload rather than one keyed by this file's own - * [TileIndex], so callers outside this package (the offline terrain contour renderer, which has its own same-shaped - * but distinct `TileIndex`) can reuse this math without a type-conversion shim. + * Places a fractional `[0,1]×[0,1]` tile-local point, [org.meshtastic.feature.map.terrain.ContourPoint]'s own + * convention, at its real-world [LatLng]. */ fun tileFractionalToLatLng(zoom: Int, tileX: Int, tileY: Int, fracX: Double, fracY: Double): LatLng { - val n = 2.0.pow(zoom) - val fx = tileX + fracX - val fy = tileY + fracY - val lon = fx / n * FULL_LON_RANGE_DEG - LON_RANGE_DEG - val latRad = atan(sinh(PI * (1 - 2 * fy / n))) - return LatLng(Math.toDegrees(latRad), lon) + val point = TerrainTileMath.lonLatAt(zoom, tileX + fracX, tileY + fracY) + return LatLng(point.latitude, point.longitude) } - - private const val LON_RANGE_DEG = 180.0 - private const val FULL_LON_RANGE_DEG = 360.0 - - /** Web Mercator's own latitude ceiling (~85.0511°), where the projection would otherwise reach infinity. */ - private const val MAX_LATITUDE = 85.05112878 } -internal data class TileIndex(val zoom: Int, val x: Int, val y: Int) +internal typealias TileIndex = org.meshtastic.feature.map.terrain.TileIndex diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/terrain/TerrainDownloadPlanner.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/terrain/TerrainDownloadPlanner.kt index bc04dd1f0a..662983e3f6 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/terrain/TerrainDownloadPlanner.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/terrain/TerrainDownloadPlanner.kt @@ -18,7 +18,7 @@ package org.meshtastic.app.map.offline.terrain import org.meshtastic.feature.map.terrain.GeoBounds import org.meshtastic.feature.map.terrain.MapterhornEndpoints -import org.meshtastic.feature.map.terrain.TerrainTileMath +import org.meshtastic.feature.map.terrain.terrainTileCount /** * Picks how deep a terrain download should go before starting one. @@ -42,32 +42,8 @@ internal object TerrainDownloadPlanner { */ fun maxZoomFitting(bounds: GeoBounds, maxTiles: Int): Int { for (zoom in MapterhornEndpoints.REGIONAL_MAX_ZOOM downTo MapterhornEndpoints.GLOBAL_MAX_ZOOM) { - if (tileCount(bounds, zoom) <= maxTiles) return zoom + if (terrainTileCount(bounds, zoom) <= maxTiles) return zoom } return MapterhornEndpoints.GLOBAL_MAX_ZOOM } - - /** - * Mirrors [org.meshtastic.feature.map.terrain.TerrainRegionExtractor.download]'s own tile enumeration, but via - * [TerrainTileMath.tileCountAt] rather than materializing each zoom level's tile list — this function walks zoom 18 - * down to 12 on every call, and materializing to count at the deep end would be the same - * count-before-you-can-afford-to-materialize problem the extractor itself was fixed for. - */ - private fun tileCount(bounds: GeoBounds, maxZoom: Int): Long { - val globalZoomRange = 0..minOf(maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM) - val globalCount = globalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - - val regionalUrl = - if (maxZoom > MapterhornEndpoints.GLOBAL_MAX_ZOOM) MapterhornEndpoints.regionalUrlFor(bounds) else null - val regionalCount = - if (regionalUrl == null) { - 0L - } else { - val regionalZoomRange = - MapterhornEndpoints.REGIONAL_MIN_ZOOM..minOf(maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM) - regionalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - } - - return globalCount + regionalCount - } } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt index aab5864350..a9b435fee7 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt @@ -17,16 +17,12 @@ package org.meshtastic.app.map.prefs.di import android.content.Context -import androidx.datastore.preferences.SharedPreferencesMigration -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.preferencesDataStoreFile -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.createPreferencesDataStore @Module @Configuration @@ -35,10 +31,6 @@ class GoogleMapsKoinModule { @Single fun provideGoogleMapsDataStore(context: Context, dispatchers: CoroutineDispatchers): GoogleMapsDataStore = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")), - scope = CoroutineScope(dispatchers.io + SupervisorJob()), - produceFile = { context.preferencesDataStoreFile("google_maps_ds") }, - ) + createPreferencesDataStore(context, dispatchers, legacyName = "google_maps_prefs", fileName = "google_maps_ds") .asGoogleMapsDataStore() } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/tiles/RasterBasemap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/tiles/RasterBasemap.kt index 4e58c391b8..c13a7a66ef 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/tiles/RasterBasemap.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/tiles/RasterBasemap.kt @@ -47,7 +47,7 @@ internal fun CustomTileProviderConfig.toRasterBasemap(): RasterBasemap? { return when { archive != null -> RasterBasemap.Local(id = id, uri = archive) - urlTemplate.isValidTileUrlTemplate(requireHttps = false) -> + urlTemplate.isValidTileUrlTemplate() -> RasterBasemap.Remote(id = id, spec = RasterTileSpec(tiles = listOf(urlTemplate))) else -> null diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitDocTranslator.kt b/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitDocTranslator.kt index 5ac72334ea..8c4154921a 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitDocTranslator.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitDocTranslator.kt @@ -23,6 +23,7 @@ import com.google.mlkit.nl.translate.TranslateLanguage import com.google.mlkit.nl.translate.TranslateRemoteModel import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.TranslatorOptions +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.suspendCancellableCoroutine import org.meshtastic.feature.docs.translation.DocTranslationCache import org.meshtastic.feature.docs.translation.DocTranslationService @@ -96,6 +97,8 @@ class MlKitDocTranslator(private val cache: DocTranslationCache) : DocTranslatio } finally { translator.close() } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.w(tag = "MlKitDocTranslator") { "Translation failed for $pageId to $targetLocale: ${e.message}" } TranslationResult.Unavailable diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitMessageTranslator.kt b/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitMessageTranslator.kt index ad821ce611..9f3767bfb8 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitMessageTranslator.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/translation/MlKitMessageTranslator.kt @@ -25,11 +25,11 @@ import com.google.mlkit.nl.translate.TranslateLanguage import com.google.mlkit.nl.translate.TranslateRemoteModel import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.TranslatorOptions +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.suspendCancellableCoroutine import org.meshtastic.feature.messaging.translation.DownloadResult import org.meshtastic.feature.messaging.translation.MessageTranslationService import org.meshtastic.feature.messaging.translation.TranslationResult -import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume /** diff --git a/androidApp/src/google/res/xml/automotive_app_desc.xml b/androidApp/src/google/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000000..77b18b928a --- /dev/null +++ b/androidApp/src/google/res/xml/automotive_app_desc.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 58c60f1c41..11fcb5165a 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -42,6 +42,8 @@ + + @@ -85,10 +87,19 @@ android:name="android.hardware.camera" android:required="false" /> + + + + + @@ -148,11 +159,6 @@ the flagship test device. --> - - - --> - - - + + + + + :core:common :core:database --> :core:model :core:database -.-> :core:di - :core:database -.-> :core:resources :core:database -.-> :core:testing classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index f408eb620a..0db74cc70a 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -28,6 +28,8 @@ kotlin { withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } + compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } + sourceSets { commonMain.dependencies { implementation(libs.androidx.sqlite.bundled) @@ -39,7 +41,6 @@ kotlin { implementation(projects.core.di) api(projects.core.model) implementation(libs.meshtastic.protobufs) - implementation(projects.core.resources) implementation(libs.androidx.room.paging) implementation(libs.kotlinx.serialization.json) implementation(libs.kermit) diff --git a/core/database/detekt-baseline.xml b/core/database/detekt-baseline.xml index c373eea433..dbcbf1362d 100644 --- a/core/database/detekt-baseline.xml +++ b/core/database/detekt-baseline.xml @@ -1,5 +1,7 @@ - + + UseOrEmpty:NodeInfoDao.kt:NodeInfoDao$getNodeByNum(num)?.node?.powerChannelLabels ?: emptyList() + diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/62.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/62.json new file mode 100644 index 0000000000..38348c5df8 --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/62.json @@ -0,0 +1,1859 @@ +{ + "formatVersion": 1, + "database": { + "version": 62, + "identityHash": "1bd762e1943093dfdd686bcedae776e8", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `soil_water_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "soilWaterTelemetry", + "columnName": "soil_water_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `display_name` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `is_maker` INTEGER NOT NULL DEFAULT 0, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "isMaker", + "columnName": "is_maker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, `last_reconciled` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastReconciled", + "columnName": "last_reconciled", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1bd762e1943093dfdd686bcedae776e8')" + ] + } +} \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/63.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/63.json new file mode 100644 index 0000000000..b557341c75 --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/63.json @@ -0,0 +1,1873 @@ +{ + "formatVersion": 1, + "database": { + "version": 63, + "identityHash": "3641f269ffa84e3748190d2993f2820f", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `soil_water_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "soilWaterTelemetry", + "columnName": "soil_water_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `display_name` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, `xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `ack_proof_status` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "xeddsaSigned", + "columnName": "xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ackProofStatus", + "columnName": "ack_proof_status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `is_maker` INTEGER NOT NULL DEFAULT 0, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "isMaker", + "columnName": "is_maker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, `last_reconciled` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastReconciled", + "columnName": "last_reconciled", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3641f269ffa84e3748190d2993f2820f')" + ] + } +} \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/64.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/64.json new file mode 100644 index 0000000000..cb14cb5d7f --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/64.json @@ -0,0 +1,1880 @@ +{ + "formatVersion": 1, + "database": { + "version": 64, + "identityHash": "bfee7bae8629a7b53c153ee1f2cb07dd", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `soil_water_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "soilWaterTelemetry", + "columnName": "soil_water_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `display_name` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_log_received_date", + "unique": false, + "columnNames": [ + "received_date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_received_date` ON `${TABLE_NAME}` (`received_date`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, `xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `ack_proof_status` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "xeddsaSigned", + "columnName": "xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ackProofStatus", + "columnName": "ack_proof_status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `is_maker` INTEGER NOT NULL DEFAULT 0, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "isMaker", + "columnName": "is_maker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, `last_reconciled` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastReconciled", + "columnName": "last_reconciled", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bfee7bae8629a7b53c153ee1f2cb07dd')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt index 9602ff75dc..ab1e7048a6 100644 --- a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt @@ -63,8 +63,13 @@ class MeshtasticDatabaseTest { @Throws(IOException::class) fun migrateAll(): Unit = runBlocking { helper.createDatabase(EARLIEST_SCHEMA_VERSION).close() - // No manual migrations: every version bump is an @AutoMigration, so Room derives the full path itself. - helper.runMigrationsAndValidate(latestSchemaVersion(), emptyList()).close() + // Every bump is an @AutoMigration except 52→53 and 63→64, which are hand-written and must be passed in. + helper + .runMigrationsAndValidate( + latestSchemaVersion(), + listOf(MeshtasticDatabase.MIGRATION_52_53, MeshtasticDatabase.MIGRATION_63_64), + ) + .close() } private fun latestSchemaVersion(): Int { diff --git a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt index 01217d9e09..bdecab88d5 100644 --- a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt @@ -44,7 +44,6 @@ import kotlin.test.assertTrue */ @RunWith(AndroidJUnit4::class) @Config(sdk = [34]) -@Suppress("MagicNumber") class DiscoveryMigrationTest { private lateinit var database: MeshtasticDatabase private lateinit var discoveryDao: DiscoveryDao @@ -190,7 +189,7 @@ class DiscoveryMigrationTest { assertNull(loaded.longitude) assertNull(loaded.distanceFromUser) assertEquals(0, loaded.hopCount) - assertEquals(0f, loaded.snr) + assertNull(loaded.snr) assertNull(loaded.rssi) assertEquals(0, loaded.messageCount) assertEquals(0, loaded.sensorPacketCount) diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/BusyTimeoutSQLiteDriver.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/BusyTimeoutSQLiteDriver.kt index de3452d3eb..fa8675381c 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/BusyTimeoutSQLiteDriver.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/BusyTimeoutSQLiteDriver.kt @@ -24,13 +24,12 @@ import androidx.sqlite.execSQL * Wraps a [SQLiteDriver] so every connection it opens waits up to [busyTimeoutMs] for a competing connection's lock * instead of failing immediately with SQLITE_BUSY ("Error code: 5, message: database is locked"). * - * Each database normally holds a single connection (see `configureCommon`), but [DatabaseManager]'s wedge recovery - * deliberately abandons a stalled connection and opens a replacement pool against the same file (see - * `abandonWedgedDbBlock`). Until the abandoned callback finishes, both connections are live — and the bundled driver's - * default busy timeout is zero, so any write on the replacement fails the moment the abandoned connection holds the - * write lock. In the field (2.8.1, build 29321949) that surfaced as a fatal uncaught SQLITE_BUSY from Room's own - * invalidation-tracker housekeeping (`TriggerBasedInvalidationTracker.syncTriggers`), which the app cannot catch. A - * busy timeout makes the replacement connection wait out the overlap instead. + * Each database normally holds a single connection (see `configureCommon`), but [DatabaseManager]'s wedge recovery can + * leave an abandoned callback's connection open beside a replacement pool on the same file (see + * `abandonWedgedDbBlock`). Recovery never publishes a replacement while another connection holds the write lock, so + * this timeout only covers overlaps that begin afterwards. Room raises any timeout below 3s on open, and runs its + * invalidation-tracker trigger sync (`TriggerBasedInvalidationTracker.syncTriggers`) under `BEGIN IMMEDIATE` with its + * SQLITE_BUSY uncaught, so a lock held past this timeout is fatal. */ class BusyTimeoutSQLiteDriver( private val delegate: SQLiteDriver, @@ -58,9 +57,9 @@ class BusyTimeoutSQLiteDriver( companion object { /** - * Long enough to ride out the typical abandoned-writer overlap (slow MeshLog cleanups and node-heavy packet - * transactions observed in the field run 1-30s), short enough that a truly stuck holder still surfaces as an - * error rather than an ANR-adjacent stall. Waiting happens on Room's I/O dispatcher, never the main thread. + * Long enough to ride out a short write on another connection, short enough that a truly stuck holder still + * surfaces as an error rather than an ANR-adjacent stall. Waiting happens on Room's I/O dispatcher, never the + * main thread. */ const val DEFAULT_BUSY_TIMEOUT_MS = 10_000L } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 9279f478bd..b2aa699a6f 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -23,6 +23,10 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey +import androidx.room3.executeSQL +import androidx.room3.immediateTransaction +import androidx.room3.useWriterConnection +import androidx.sqlite.SQLiteException import co.touchlab.kermit.Logger import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.SynchronizedObject @@ -33,6 +37,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -135,6 +140,15 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val FLOW_OBSERVER, } + private sealed interface ReopenResult { + data class Replaced(val database: MeshtasticDatabase) : ReopenResult + + /** Another connection holds the database file's write lock, so the pool is busy rather than wedged. */ + data object WriteLockHeld : ReopenResult + + data object NotReplaced : ReopenResult + } + @Volatile private var lifecycleState = LifecycleState.OPEN // Per-database bounded-access tracking and per-source write barrier for merges. `withDb` deliberately does NOT @@ -415,6 +429,7 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val // build a replacement and abandon the cache/publication handoff halfway through. withContext(NonCancellable) { reopenFlowDatabaseIfStillCurrent(database) } } catch (@Suppress("TooGenericExceptionCaught") recoveryFailure: Exception) { + if (recoveryFailure is CancellationException) currentCoroutineContext().ensureActive() exception.addSuppressed(recoveryFailure) Logger.w(recoveryFailure) { "Failed to recover active DB after a Flow pool timeout" } throw exception @@ -763,21 +778,22 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val // clear a later association's gate. suspend fun releaseWriterGate(canonicalDb: MeshtasticDatabase, canonicalName: String) { withContext(NonCancellable) { - val released = - writerTrackerMutex.withLock { - if (writerGate !== gate) { - false - } else { - writerGate = null - _currentDb.value = canonicalDb - currentDbName = canonicalName - true - } + val released = writerTrackerMutex.withLock { + if (writerGate !== gate) { + false + } else { + writerGate = null + _currentDb.value = canonicalDb + currentDbName = canonicalName + true } + } if (released) gate.complete(canonicalDb) } } + // Runs under NonCancellable, so no cancellation of this coroutine can reach the catch. + @Suppress("SuspendFunSwallowedCancellation") suspend fun clearPendingRouteBestEffort() { withContext(NonCancellable) { try { @@ -912,6 +928,8 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val private suspend fun persistRetirementIntent(dbName: String) { try { datastore.edit { it[retiredDbNamesKey] = it[retiredDbNamesKey].orEmpty() + dbName } + } catch (cancellation: CancellationException) { + throw cancellation } catch (failure: Throwable) { Logger.w(failure) { "Failed to persist retirement for ${anonymizeDbName(dbName)}" } } @@ -957,6 +975,8 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val if (remaining.isEmpty()) it.remove(retiredDbNamesKey) else it[retiredDbNamesKey] = remaining } Logger.i { "Physically retired merged DB ${anonymizeDbName(dbName)}" } + } catch (cancellation: CancellationException) { + throw cancellation } catch (failure: Throwable) { // The file deletion is idempotent. Retain the intent so a later process retries metadata cleanup. Logger.w(failure) { "Failed to clear retirement metadata for ${anonymizeDbName(dbName)}" } @@ -1002,15 +1022,16 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val * directly, so every publication is visible to app-wide collectors on the same program step — but there is no * deterministic handoff point where every collector has stopped using the previous instance. * - * Returns the reopened DB, or null if another coroutine switched databases or shutdown has started. + * Returns the reopened DB, or null if another coroutine switched databases, shutdown has started, the recovery rate + * limit is reached, or another connection holds the database's write lock. */ private suspend fun reopenFlowDatabaseIfStillCurrent(expectedDb: MeshtasticDatabase): MeshtasticDatabase? { - val expectedDbName = - mutex.withLock { - if (lifecycleState != LifecycleState.OPEN || _currentDb.value !== expectedDb) return null - currentDbName - } - return reopenActiveDatabaseIfStillCurrent(expectedDb, expectedDbName, ReopenOrigin.FLOW_OBSERVER) + val expectedDbName = mutex.withLock { + if (lifecycleState != LifecycleState.OPEN || _currentDb.value !== expectedDb) return null + currentDbName + } + val result = reopenActiveDatabaseIfStillCurrent(expectedDb, expectedDbName, ReopenOrigin.FLOW_OBSERVER) + return (result as? ReopenResult.Replaced)?.database } /** Per-window budget for [origin], or null when [origin] needs no budget of its own. */ @@ -1063,11 +1084,13 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val expectedDb: MeshtasticDatabase, expectedDbName: String, origin: ReopenOrigin, - ): MeshtasticDatabase? = withManagerOperation { + ): ReopenResult = withManagerOperation { mutex.withLock { - if (lifecycleState != LifecycleState.OPEN) return@withManagerOperation null - if (_currentDb.value !== expectedDb || currentDbName != expectedDbName) return@withManagerOperation null - if (hasReachedRecoveryLimit(origin)) return@withManagerOperation null + if (lifecycleState != LifecycleState.OPEN) return@withManagerOperation ReopenResult.NotReplaced + if (_currentDb.value !== expectedDb || currentDbName != expectedDbName) { + return@withManagerOperation ReopenResult.NotReplaced + } + if (hasReachedRecoveryLimit(origin)) return@withManagerOperation ReopenResult.NotReplaced val registered = dbCache[expectedDbName] @@ -1078,19 +1101,19 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val } if (registered !== expectedDb) { Logger.w { "DB recovery: active registration changed before reopen; skipping active DB reopen" } - return@withManagerOperation null + return@withManagerOperation ReopenResult.NotReplaced } // Build a fresh instance directly (not through getOrPut) before touching the cache, // so a failed or cancelled build leaves the existing cache entry and _currentDb consistent. val reopened = withContext(dispatchers.io) { buildDatabase(expectedDbName) } + if (!isPublishableReplacement(expectedDbName, reopened)) { + return@withManagerOperation ReopenResult.WriteLockHeld + } + // After the probe's suspension, so a shutdown that began during it closes the replacement instead. if (lifecycleState != LifecycleState.OPEN) { - runCatching { closeDatabase(reopened) } - .onFailure { - detachedDatabases.add(NamedDatabase(expectedDbName, reopened)) - Logger.w(it) { "Failed to close database built during shutdown; retained for shutdown retry" } - } - return@withManagerOperation null + closeUnpublishedDatabase(expectedDbName, reopened) + return@withManagerOperation ReopenResult.NotReplaced } dbCache[expectedDbName] = reopened if (expectedDbName == DatabaseConstants.DEFAULT_DB_NAME) { @@ -1119,10 +1142,69 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val // `currentDb.value` (see PacketRepositoryImpl), so no counter here can show a pool is unreachable. // [POOL_RECOVERY_WINDOW_MS] bounds how fast replacements can be created instead. - reopened + ReopenResult.Replaced(reopened) } } + /** + * Decides whether an unpublished [replacement] may be published, closing it if not. + * + * A replacement shares its file, and so its write lock, with every older pool. While another connection holds that + * lock the replacement cannot write either, and Room syncs its invalidation triggers under `BEGIN IMMEDIATE` on + * first use, where SQLITE_BUSY escapes uncaught. An abandoned block that holds the lock is still writing, so its + * pool is slow rather than wedged and stays published. + */ + @Suppress("TooGenericExceptionCaught", "SuspendFunSwallowedCancellation") // closes the replacement, then rethrows + private suspend fun isPublishableReplacement(dbName: String, replacement: MeshtasticDatabase): Boolean { + val lockHeld = + try { + isWriteLockHeldElsewhere(replacement) + } catch (probeFailure: Throwable) { + closeUnpublishedDatabase(dbName, replacement) + throw probeFailure + } + if (lockHeld) { + closeUnpublishedDatabase(dbName, replacement) + Logger.w { "DB recovery: another connection holds the database write lock; keeping the active pool" } + } + return !lockHeld + } + + /** + * Reports whether another connection holds [database]'s write lock, asked on [database]'s own connection. The + * transaction itself does not wait, but opening that connection can: Room raises the busy timeout to at least 3s + * before its first statement, and under a rollback journal an EXCLUSIVE holder blocks that open too. A locked open + * is the same verdict. In-memory test databases override this: they never share a file, so nothing else can hold + * it. + */ + protected open suspend fun isWriteLockHeldElsewhere(database: MeshtasticDatabase): Boolean = try { + database.useWriterConnection { connection -> + val busyTimeoutMs = + connection.usePrepared("PRAGMA busy_timeout") { statement -> + statement.step() + statement.getLong(0) + } + connection.executeSQL("PRAGMA busy_timeout = 0") + connection.immediateTransaction {} + // Restored only once the lock proved free: a held verdict closes this pool, and at zero Room's post-use + // refresh fails fast instead of holding up that close for the whole busy timeout. + connection.executeSQL("PRAGMA busy_timeout = $busyTimeoutMs") + } + false + } catch (busy: SQLiteException) { + if (!isDbLockedException(busy)) throw busy + true + } + + /** Closes a pool that was never published, retaining it for shutdown if the close fails. Caller holds [mutex]. */ + private fun closeUnpublishedDatabase(dbName: String, database: MeshtasticDatabase) { + runCatching { closeDatabase(database) } + .onFailure { + detachedDatabases.add(NamedDatabase(dbName, database)) + Logger.w(it) { "Failed to close an unpublished database; retained for shutdown retry" } + } + } + /** Test-only visibility for detached-pool retention. */ internal suspend fun debugDetachedPoolCount(): Int = mutex.withLock { detachedDatabases.size } @@ -1148,7 +1230,8 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val * section is never torn apart mid-write. A block that never returns (Room 3.x logs and retries connection-pool * acquisition instead of throwing, so a leaked permit hangs forever) is abandoned rather than cancelled: the caller * fails with [DatabaseOperationTimeoutException] and the active pool is reopened, so later calls are admitted - * against the replacement pool and its fresh lane instead of queueing behind the wedge. + * against the replacement pool and its fresh lane instead of queueing behind the wedge. A block still holding the + * database's write lock is slow rather than wedged, so its pool is kept and later calls wait for the lock. * * Long-lived Flow/Paging reads must stay out of `withDb`; see [observeCurrentDb] and [withReadDb]. */ @@ -1186,6 +1269,7 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val * only once it is about to invoke the callback — a call still waiting for its lane has performed no side effect and * is aborted at the cancellation check instead of running late. */ + @OptIn(DelicateCoroutinesApi::class) private fun launchDbBlock( admission: AdmittedDatabase, blockStarted: CompletableDeferred, @@ -1212,14 +1296,13 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val */ private suspend fun quarantineWedgedPoolIfStillPublished(database: MeshtasticDatabase) { if (lifecycleState != LifecycleState.OPEN) return - val quarantined = - writerTrackerMutex.withLock { - if (lifecycleState != LifecycleState.OPEN || _currentDb.value !== database) { - false - } else { - unrecoverablePools.put(database, recoveryNowMillis()) == null - } + val quarantined = writerTrackerMutex.withLock { + if (lifecycleState != LifecycleState.OPEN || _currentDb.value !== database) { + false + } else { + unrecoverablePools.put(database, recoveryNowMillis()) == null } + } if (quarantined) { Logger.w { "Marked the active DB pool unrecoverable after wedge recovery was refused; database operations fail " + @@ -1235,7 +1318,9 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val * A started block is not cancellable by design, so it keeps its lane and writer registration until it returns — a * merge drain or shutdown drain that waits on it still resolves on its own bound and logs. Reopening the active * pool is what unwedges the app: the wedged instance moves to the detached set (protected from eviction, reclaimed - * by [close]) and later callers admit against the replacement pool and its fresh lane. + * by [close]) and later callers admit against the replacement pool and its fresh lane. When another connection + * still holds the write lock the pool is kept and not quarantined: it is working, and a replacement could not + * write. */ @Suppress("TooGenericExceptionCaught", "ThrowsCount") private suspend fun abandonWedgedDbBlock( @@ -1262,15 +1347,21 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val throw recoveryCancel } catch (recoveryFailure: Exception) { Logger.w(recoveryFailure) { "withDb: failed to reopen active DB after abandoning a wedged callback" } - null + ReopenResult.NotReplaced } - if (reopened == null) quarantineWedgedPoolIfStillPublished(admission.database) + if (reopened == ReopenResult.NotReplaced) quarantineWedgedPoolIfStillPublished(admission.database) Logger.w { - if (reopened != null) { - "withDb callback exceeded ${timeoutMillis}ms; abandoned it and reopened the active DB so later " + - "calls run on a fresh connection pool" - } else { - "withDb callback exceeded ${timeoutMillis}ms; abandoned it but the active DB was not reopened" + when (reopened) { + is ReopenResult.Replaced -> + "withDb callback exceeded ${timeoutMillis}ms; abandoned it and reopened the active DB so later " + + "calls run on a fresh connection pool" + + ReopenResult.WriteLockHeld -> + "withDb callback exceeded ${timeoutMillis}ms; abandoned it and kept the active DB because " + + "another connection still holds its write lock" + + ReopenResult.NotReplaced -> + "withDb callback exceeded ${timeoutMillis}ms; abandoned it but the active DB was not reopened" } } throw DatabaseOperationTimeoutException( @@ -1306,16 +1397,15 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val /** Releases a bounded reader and retries an eviction that was deferred while the captured pool was in use. */ private suspend fun endRead(database: MeshtasticDatabase) { - val retryEviction = - writerTrackerMutex.withLock { - val remaining = (activeReaders[database] ?: 1) - 1 - if (remaining <= 0) { - activeReaders.remove(database) - } else { - activeReaders[database] = remaining - } - !hasActiveDatabaseAccessLocked(database) && deferredEvictions.remove(database) + val retryEviction = writerTrackerMutex.withLock { + val remaining = (activeReaders[database] ?: 1) - 1 + if (remaining <= 0) { + activeReaders.remove(database) + } else { + activeReaders[database] = remaining } + !hasActiveDatabaseAccessLocked(database) && deferredEvictions.remove(database) + } if (retryEviction && lifecycleState == LifecycleState.OPEN) { launchManagerWork(dispatchers.io) { enforceCacheLimit() } } @@ -1368,28 +1458,27 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val private suspend fun beginWrite(): AdmittedDatabase { while (true) { var admitted: AdmittedDatabase? = null - val gate = - writerTrackerMutex.withLock { - checkOpen() - val pendingGate = writerGate - if (pendingGate == null) { - val db = _currentDb.value - if (isQuarantinedLocked(db)) { - throw DatabaseOperationTimeoutException( - "database pool is wedged and its replacement budget is spent; refusing to start " + - "another operation that cannot finish", - ) - } - activeWriters[db] = (activeWriters[db] ?: 0) + 1 - admitted = - AdmittedDatabase( - database = db, - name = currentDbName, - lane = poolLanes.getOrPut(db) { createPoolLane() }, - ) + val gate = writerTrackerMutex.withLock { + checkOpen() + val pendingGate = writerGate + if (pendingGate == null) { + val db = _currentDb.value + if (isQuarantinedLocked(db)) { + throw DatabaseOperationTimeoutException( + "database pool is wedged and its replacement budget is spent; refusing to start " + + "another operation that cannot finish", + ) } - pendingGate + activeWriters[db] = (activeWriters[db] ?: 0) + 1 + admitted = + AdmittedDatabase( + database = db, + name = currentDbName, + lane = poolLanes.getOrPut(db) { createPoolLane() }, + ) } + pendingGate + } admitted?.let { return it } @@ -1399,29 +1488,26 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val pendingGate.await() true } - if (released == null) { - throw IllegalStateException( - "Timed out waiting ${WRITER_GATE_TIMEOUT_MS}ms for database writer admission gate", - ) + checkNotNull(released) { + "Timed out waiting ${WRITER_GATE_TIMEOUT_MS}ms for database writer admission gate" } } } /** Deregisters a writer and releases any merge waiting for [db] to quiesce. Cancellation-safe (see call site). */ private suspend fun endWrite(db: MeshtasticDatabase) { - val retryEviction = - writerTrackerMutex.withLock { - val remaining = (activeWriters[db] ?: 1) - 1 - val drained = remaining <= 0 - if (drained) { - activeWriters.remove(db) - drainWaiters.remove(db)?.forEach { it.complete(Unit) } - } else { - activeWriters[db] = remaining - } - if (activeWriters.isEmpty()) shutdownWriterDrain?.complete(Unit) - !hasActiveDatabaseAccessLocked(db) && deferredEvictions.remove(db) + val retryEviction = writerTrackerMutex.withLock { + val remaining = (activeWriters[db] ?: 1) - 1 + val drained = remaining <= 0 + if (drained) { + activeWriters.remove(db) + drainWaiters.remove(db)?.forEach { it.complete(Unit) } + } else { + activeWriters[db] = remaining } + if (activeWriters.isEmpty()) shutdownWriterDrain?.complete(Unit) + !hasActiveDatabaseAccessLocked(db) && deferredEvictions.remove(db) + } if (retryEviction && lifecycleState == LifecycleState.OPEN) { launchManagerWork(dispatchers.io) { enforceCacheLimit() } } @@ -1446,8 +1532,9 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val * every association attempt has released its gate and drained its source — a non-zero pair after a quiescent period * indicates a leaked writer or waiter. */ - internal suspend fun debugWriterCounts(): Pair = - writerTrackerMutex.withLock { activeWriters.values.sum() to drainWaiters.values.sumOf { it.size } } + internal suspend fun debugWriterCounts(): Pair = writerTrackerMutex.withLock { + activeWriters.values.sum() to drainWaiters.values.sumOf { it.size } + } internal suspend fun debugReaderCount(database: MeshtasticDatabase? = null): Int = writerTrackerMutex.withLock { if (database == null) activeReaders.values.sum() else activeReaders[database] ?: 0 @@ -1459,8 +1546,9 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val internal suspend fun debugWriterGateArmed(): Boolean = writerTrackerMutex.withLock { writerGate != null } /** Test-only visibility for cancellation-atomic pending-route recovery assertions. */ - internal suspend fun debugIsLogicallyRetired(dbName: String): Boolean = - mutex.withLock { dbName in logicallyRetired } + internal suspend fun debugIsLogicallyRetired(dbName: String): Boolean = mutex.withLock { + dbName in logicallyRetired + } /** Test-only visibility for deterministic shutdown assertions. */ internal fun debugAcceptingWrites(): Boolean = lifecycleState == LifecycleState.OPEN @@ -1479,11 +1567,10 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val */ @Suppress("ReturnCount") private suspend fun drainWriters(db: MeshtasticDatabase, dbName: String): Boolean { - val waiter = - writerTrackerMutex.withLock { - if ((activeWriters[db] ?: 0) == 0) return true - CompletableDeferred().also { drainWaiters.getOrPut(db) { mutableListOf() }.add(it) } - } + val waiter = writerTrackerMutex.withLock { + if ((activeWriters[db] ?: 0) == 0) return true + CompletableDeferred().also { drainWaiters.getOrPut(db) { mutableListOf() }.add(it) } + } try { val drained = withTimeoutOrNull(WRITER_DRAIN_TIMEOUT_MS) { waiter.await() } if (drained == null) { @@ -1538,7 +1625,8 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val if (currentDb === db && isDbPoolAcquireTimeoutException(e)) { val reopened = try { - reopenActiveDatabaseIfStillCurrent(db, active, ReopenOrigin.BOUNDED_OPERATION) + val result = reopenActiveDatabaseIfStillCurrent(db, active, ReopenOrigin.BOUNDED_OPERATION) + (result as? ReopenResult.Replaced)?.database } catch (recoveryCancel: CancellationException) { throw recoveryCancel } catch (recoveryFailure: Exception) { @@ -1595,6 +1683,7 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val private const val ROOM_POOL_ACQUIRE_TIMEOUT_PHRASE = "timed out attempting to acquire" private const val ROOM_READER_CONNECTION_PHRASE = "reader connection" private const val ROOM_WRITER_CONNECTION_PHRASE = "writer connection" + private const val SQLITE_BUSY_PHRASE = "database is locked" /** * Room KMP currently exposes pool-acquire timeouts as exception message text instead of a stable common typed @@ -1615,8 +1704,11 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val .any { throwable -> val msg = throwable.message?.lowercase() ?: return@any false val hasDbContext = DB_TERMS.any { it in msg } - ("closed" in msg && hasDbContext) || "database is locked" in msg || "sqlite_busy" in msg + ("closed" in msg && hasDbContext) || SQLITE_BUSY_PHRASE in msg || "sqlite_busy" in msg } + + private fun isDbLockedException(e: Throwable): Boolean = + e.message?.contains(SQLITE_BUSY_PHRASE, ignoreCase = true) == true } /** @@ -1713,18 +1805,17 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val lastUsedMsByDb = usageSnapshot, protectedDbNames = pendingRouteNames, ) - val evictableVictims = - writerTrackerMutex.withLock { - victims.filter { name -> - val cached = dbCache[name] - if (cached != null && hasActiveDatabaseAccessLocked(cached)) { - deferredEvictions.add(cached) - false - } else { - true - } + val evictableVictims = writerTrackerMutex.withLock { + victims.filter { name -> + val cached = dbCache[name] + if (cached != null && hasActiveDatabaseAccessLocked(cached)) { + deferredEvictions.add(cached) + false + } else { + true } } + } evictableVictims.forEach { name -> try { @@ -1832,33 +1923,32 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val var jobsDrain: CompletableDeferred? = null var closedSuccessfully = false - val shouldClose = - writerTrackerMutex.withLock { - when (lifecycleState) { - LifecycleState.CLOSED -> false + val shouldClose = writerTrackerMutex.withLock { + when (lifecycleState) { + LifecycleState.CLOSED -> false - LifecycleState.OPEN, - LifecycleState.CLOSING, - -> { - lifecycleState = LifecycleState.CLOSING - operationsDrain = - if (activeManagerOperations.isNotEmpty()) { - CompletableDeferred().also { managerOperationDrain = it } - } else { - managerOperationDrain = null - null - } - writerDrain = - if (activeWriters.isNotEmpty()) { - CompletableDeferred().also { shutdownWriterDrain = it } - } else { - shutdownWriterDrain = null - null - } - true - } + LifecycleState.OPEN, + LifecycleState.CLOSING, + -> { + lifecycleState = LifecycleState.CLOSING + operationsDrain = + if (activeManagerOperations.isNotEmpty()) { + CompletableDeferred().also { managerOperationDrain = it } + } else { + managerOperationDrain = null + null + } + writerDrain = + if (activeWriters.isNotEmpty()) { + CompletableDeferred().also { shutdownWriterDrain = it } + } else { + shutdownWriterDrain = null + null + } + true } } + } if (!shouldClose) return@closeAttempt val managerJobs = @@ -1884,6 +1974,9 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val // while CLOSING, and a timed-out attempt leaves ownership intact so a later close() can retry. val operationsDrained = operationsDrain?.let { drain -> + // The drain timeout measures real time; on an injected test dispatcher it would expire at + // once. + @Suppress("InjectDispatcher") val completedBeforeTimeout = withContext(Dispatchers.Default) { withTimeoutOrNull(WRITER_DRAIN_TIMEOUT_MS) { @@ -1922,38 +2015,37 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val // Snapshot ownership without transferring it yet. A failed pool close must leave every instance and // retirement intent reachable by a later close() attempt. - val snapshot = - mutex.withLock { - val databases = mutableListOf() - fun addDistinct(dbName: String, database: MeshtasticDatabase?) { - if (database == null) return - val existing = databases.firstOrNull { it.database === database } - if (existing == null) { - databases.add(ShutdownDatabase(database, mutableSetOf(dbName))) - } else { - existing.dbNames.add(dbName) - } + val snapshot = mutex.withLock { + val databases = mutableListOf() + fun addDistinct(dbName: String, database: MeshtasticDatabase?) { + if (database == null) return + val existing = databases.firstOrNull { it.database === database } + if (existing == null) { + databases.add(ShutdownDatabase(database, mutableSetOf(dbName))) + } else { + existing.dbNames.add(dbName) } - - dbCache.forEach { (dbName, database) -> addDistinct(dbName, database) } - detachedDatabases.forEach { addDistinct(it.dbName, it.database) } - synchronized(initializationLock) { - addDistinct(DatabaseConstants.DEFAULT_DB_NAME, initializedDefaultDb) - addDistinct(currentDbName, currentDbState?.value) - } - - val persistedRetiredNames = - try { - datastore.data.first()[retiredDbNamesKey].orEmpty() - } catch (failure: Throwable) { - Logger.w(failure) { - "Failed to read persisted database retirements during shutdown" - } - emptySet() - } - ShutdownSnapshot(databases, (logicallyRetired + persistedRetiredNames).toList()) } + dbCache.forEach { (dbName, database) -> addDistinct(dbName, database) } + detachedDatabases.forEach { addDistinct(it.dbName, it.database) } + synchronized(initializationLock) { + addDistinct(DatabaseConstants.DEFAULT_DB_NAME, initializedDefaultDb) + addDistinct(currentDbName, currentDbState?.value) + } + + val persistedRetiredNames = + try { + datastore.data.first()[retiredDbNamesKey].orEmpty() + } catch (failure: Throwable) { + Logger.w(failure) { + "Failed to read persisted database retirements during shutdown" + } + emptySet() + } + ShutdownSnapshot(databases, (logicallyRetired + persistedRetiredNames).toList()) + } + // All tracked work has stopped. The remaining scope-owned collector does not touch Room; cancel it // before closing pools, but keep ownership maps intact until every close succeeds. managerScope.coroutineContext[Job]?.cancel() diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index dd88d8abef..e6ee03491e 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -149,8 +149,11 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity AutoMigration(from = 58, to = 59), AutoMigration(from = 59, to = 60), AutoMigration(from = 60, to = 61), + AutoMigration(from = 61, to = 62), + AutoMigration(from = 62, to = 63), + // 63 -> 64 is the manual MIGRATION_63_64 (log index added in place), applied via configureCommon(). ], - version = 61, + version = 64, exportSchema = true, ) @androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class) @@ -207,6 +210,55 @@ abstract class MeshtasticDatabase : RoomDatabase() { } } + /** + * Makes `discovered_node.snr` nullable and indexes `log.received_date`. + * + * Room's auto-migration for this step rebuilds `log` too: it copies every log row, `from_radio` blobs included, + * and drops the parent table of `traceroute_node_position`, all inside the exclusive migration transaction. + * Only `discovered_node` needs a rebuild, because SQLite cannot drop NOT NULL in place, so its statements below + * are Room's generated copy-swap verbatim, and the log index is created in place. + */ + internal val MIGRATION_63_64: Migration = + object : Migration(63, 64) { + override suspend fun migrate(connection: SQLiteConnection) { + connection.execSQL( + "CREATE TABLE IF NOT EXISTS `_new_discovered_node` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, " + + "`node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, " + + "`neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, " + + "`distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL, " + + "`rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, " + + "`sensor_packet_count` INTEGER NOT NULL DEFAULT 0, " + + "`is_infrastructure` INTEGER NOT NULL DEFAULT 0, " + + "FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) " + + "ON UPDATE NO ACTION ON DELETE CASCADE )", + ) + connection.execSQL( + "INSERT INTO `_new_discovered_node` (`id`,`preset_result_id`,`node_num`,`short_name`," + + "`long_name`,`neighbor_type`,`latitude`,`longitude`,`distance_from_user`,`hop_count`," + + "`snr`,`rssi`,`message_count`,`sensor_packet_count`,`is_infrastructure`) " + + "SELECT `id`,`preset_result_id`,`node_num`,`short_name`,`long_name`,`neighbor_type`," + + "`latitude`,`longitude`,`distance_from_user`,`hop_count`,`snr`,`rssi`,`message_count`," + + "`sensor_packet_count`,`is_infrastructure` FROM `discovered_node`", + ) + connection.execSQL("DROP TABLE `discovered_node`") + connection.execSQL("ALTER TABLE `_new_discovered_node` RENAME TO `discovered_node`") + connection.execSQL( + "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` " + + "ON `discovered_node` (`preset_result_id`)", + ) + connection.execSQL( + "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `discovered_node` (`node_num`)", + ) + connection.prepare("PRAGMA foreign_key_check(`discovered_node`)").use { violations -> + check(!violations.step()) { "discovered_node has rows whose preset result does not exist" } + } + connection.execSQL( + "CREATE INDEX IF NOT EXISTS `index_log_received_date` ON `log` (`received_date`)", + ) + } + } + /** * Configures a [RoomDatabase.Builder] with standard settings for this project. * @@ -223,7 +275,7 @@ abstract class MeshtasticDatabase : RoomDatabase() { @OptIn(ExperimentalCoroutinesApi::class) fun RoomDatabase.Builder.configureCommon(): RoomDatabase.Builder = this.fallbackToDestructiveMigration(dropAllTables = false) - .addMigrations(MIGRATION_52_53) + .addMigrations(MIGRATION_52_53, MIGRATION_63_64) .setSingleConnectionPool() .setQueryCoroutineContext( // limitedParallelism(1) has the same throughput ceiling as the single-connection pool diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt index 0bb86ad47e..15402760c8 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt @@ -134,6 +134,10 @@ interface DiscoveryDao { @Query("SELECT * FROM discovered_node WHERE preset_result_id = :presetResultId") fun getDiscoveredNodesFlow(presetResultId: Long): Flow> + /** Nodes of every preset result in [presetResultIds]; callers keep the list under the bind-parameter limit. */ + @Query("SELECT * FROM discovered_node WHERE preset_result_id IN (:presetResultIds) ORDER BY id") + suspend fun getDiscoveredNodesForPresetResults(presetResultIds: List): List + @Query( """ SELECT DISTINCT node_num FROM discovered_node dn diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt index 77e8f9aad9..1aba7b0601 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt @@ -21,7 +21,7 @@ import androidx.room3.Query import androidx.room3.Transaction import androidx.room3.Upsert import org.meshtastic.core.database.entity.FirmwareReleaseEntity -import org.meshtastic.core.database.entity.FirmwareReleaseType +import org.meshtastic.core.model.FirmwareReleaseType @Dao interface FirmwareReleaseDao { diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt index 878bb25e61..8c93080c0d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt @@ -24,6 +24,7 @@ import androidx.room3.Transaction import kotlinx.coroutines.flow.Flow import org.meshtastic.core.database.DatabaseConstants.SQLITE_MAX_BIND_PARAMETERS import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.core.database.entity.MeshLogRow @Dao @Suppress("TooManyFunctions") @@ -42,8 +43,20 @@ interface MeshLogDao { @Query("SELECT * FROM log ORDER BY received_date DESC LIMIT :maxItem") fun getAllLogs(maxItem: Int): Flow> - @Query("SELECT * FROM log ORDER BY received_date ASC LIMIT :maxItem") - fun getAllLogsInReceiveOrder(maxItem: Int): Flow> + /** + * Returns up to [pageSize] logs after ([afterReceivedDate], [afterRowId]), oldest first. Equal received dates stay + * in rowid order, which is the order a scan of the received_date index returns. Start from [Long.MIN_VALUE] for + * both and continue from the last row returned. + */ + @Query( + """ + SELECT rowid AS log_rowid, * FROM log + WHERE (received_date, rowid) > (:afterReceivedDate, :afterRowId) + ORDER BY received_date ASC, rowid ASC + LIMIT :pageSize + """, + ) + suspend fun getLogsInReceiveOrderAfter(afterReceivedDate: Long, afterRowId: Long, pageSize: Int): List /** * Retrieves [MeshLog]s matching 'from_num' (nodeNum) and 'port_num' (PortNum). @@ -77,8 +90,18 @@ interface MeshLogDao { @Query("DELETE FROM log WHERE from_num = :fromNum AND port_num = :portNum") suspend fun deleteLogs(fromNum: Int, portNum: Int) - @Query("DELETE FROM log WHERE received_date < :cutoffTimestamp") - suspend fun deleteOlderThan(cutoffTimestamp: Long) + /** + * Deletes at most [limit] logs received before [cutoffTimestamp] and returns how many it removed. Callers repeat it + * until it removes fewer than [limit], so a retention pass never holds the write lock for the whole backlog. + */ + @Query( + """ + DELETE FROM log WHERE rowid IN ( + SELECT rowid FROM log WHERE received_date < :cutoffTimestamp LIMIT :limit + ) + """, + ) + suspend fun deleteOlderThan(cutoffTimestamp: Long, limit: Int): Int /** * Suspend snapshot variant of [getLogsFrom] for one-shot reads (no Flow observer overhead). Used when a caller diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt index adaa018229..c6ee596afd 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt @@ -236,7 +236,7 @@ interface NodeInfoDao { val isPlaceholder = incomingNode.user.hw_model == HardwareModel.UNSET val hasExistingUser = existingNode.user.hw_model != HardwareModel.UNSET - val isDefaultName = incomingNode.user.long_name.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$")) + val isDefaultName = incomingNode.user.long_name.matches(DEFAULT_NODE_NAME) if (hasExistingUser && isPlaceholder && isDefaultName) { return incomingNode.copy( @@ -676,3 +676,6 @@ interface NodeInfoDao { @Query("SELECT * FROM nodes") suspend fun getAllNodesSnapshot(): List } + +/** The name firmware gives a node that has not set one, compiled once for the upsert path. */ +private val DEFAULT_NODE_NAME = Regex("^Meshtastic [0-9a-fA-F]{4}$") diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt index 041fc5428d..3e54cfb63d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt @@ -39,6 +39,7 @@ import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.util.ChannelKeyChange import org.meshtastic.core.model.util.ConversationSlot +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.proto.MeshPacket @Suppress("TooManyFunctions", "LargeClass") @@ -351,8 +352,7 @@ interface PacketDao { */ @Transaction suspend fun applyOutgoingReactionQueueStatus(packetId: Int, status: MessageStatus): ReactionEntity? { - val match = - findReactionsWithId(packetId).filter { it.status != MessageStatus.RECEIVED }.singleOrNull() ?: return null + val match = findReactionsWithId(packetId).singleOrNull { it.status != MessageStatus.RECEIVED } ?: return null if (shouldApplyOutgoingQueueStatus(match.status, status)) update(match.copy(status = status)) return match } @@ -1006,7 +1006,8 @@ interface PacketDao { private fun MessageStatus.isDowngradeFrom(current: MessageStatus?) = current == MessageStatus.SFPP_CONFIRMED && this == MessageStatus.SFPP_ROUTING - private fun resolveNewTime(rxTime: Long, fallback: Long) = if (rxTime > 0) rxTime * MILLIS_PER_SECOND else fallback + private fun resolveNewTime(rxTime: Long, fallback: Long) = + if (rxTime > 0) rxTime * TimeConstants.MS_PER_SEC else fallback /** * Atomically applies an SFPP delivery-status transition to every packet and reaction matching [packetId] + address @@ -1090,10 +1091,6 @@ interface PacketDao { // endregion - companion object { - private const val MILLIS_PER_SECOND = 1000L - } - // region ── FTS5 Search ── @Query( @@ -1197,10 +1194,9 @@ private suspend fun PacketDao.applyLiveMoves(liveMoves: List) } else { getAllUserPacketsForMigration().filter { ContactKey(it.contact_key).channelOrNull in sourceIndices } } - val moveByIndex = - liveMoves.associate { change -> - (change.from as ConversationSlot.Live).index to (change.to as ConversationSlot.Live).index - } + val moveByIndex = liveMoves.associate { change -> + (change.from as ConversationSlot.Live).index to (change.to as ConversationSlot.Live).index + } // Settings are re-keyed the same way: read every affected row, then rewrite, so a swap cannot land a // conversation's mute or pin on the channel it traded places with. diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt index 1b3bea9d38..1c6ee7452f 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt @@ -24,8 +24,8 @@ import org.meshtastic.core.database.entity.DiscoverySessionEntity /** * A switch-aware [DiscoveryDao] that resolves the active database on every call instead of pinning the one that was - * current at injection time. This is what Koin hands to `feature:discovery` consumers (ViewModels and the scan engine), - * which hold their DAO for their whole lifetime: + * current at injection time. This is what Koin hands to `DiscoveryRepositoryImpl`, which the discovery ViewModels read + * through, and to the scan engine and its coordinators; all of them hold it for their whole lifetime: * - Flow methods re-latch through [DatabaseProvider.observeCurrentDb], so an open discovery screen follows a device/DB * switch and recovers from a wedged active Room pool. * - Suspend methods go through [DatabaseProvider.withDb], so writes register with the cross-transport merge drain @@ -150,6 +150,9 @@ class SwitchingDiscoveryDao(private val dbManager: DatabaseProvider) : Discovery override fun getDiscoveredNodesFlow(presetResultId: Long): Flow> = dbManager.observeCurrentDb { it.discoveryDao().getDiscoveredNodesFlow(presetResultId) } + override suspend fun getDiscoveredNodesForPresetResults(presetResultIds: List): List = + dbManager.withDb { it.discoveryDao().getDiscoveredNodesForPresetResults(presetResultIds) }.orEmpty() + override suspend fun getUniqueNodeNums(sessionId: Long): List = dbManager.withDb { it.discoveryDao().getUniqueNodeNums(sessionId) }.orEmpty() diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt index cd7e4579cf..dfe3469e3c 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt @@ -33,7 +33,7 @@ class CoreDatabaseModule { createDatabaseDataStore("db-manager-prefs").asDatabaseDataStore() /** - * Long-lived consumers (discovery ViewModels, the scan engine) hold this DAO across device/DB switches, so hand + * Long-lived consumers (the discovery repository, the scan engine) hold this DAO across device/DB switches, so hand * them the switch-aware delegate — never a DAO pinned to the injection-time `currentDb.value`, which would keep * reading a stale DB and crash once a cross-transport merge retires it. See [SwitchingDiscoveryDao]. */ diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt index 4c4112432d..c538facfe3 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt @@ -35,6 +35,7 @@ data class DeviceHardwareEntity( val hwModel: Int, @ColumnInfo(name = "hw_model_slug") val hwModelSlug: String, val images: List?, + @ColumnInfo(name = "is_maker", defaultValue = "0") val isMaker: Boolean = false, @ColumnInfo(name = "last_updated") val lastUpdated: Long = nowMillis, @ColumnInfo(name = "partition_scheme") val partitionScheme: String? = null, @PrimaryKey @ColumnInfo(name = "platformio_target") val platformioTarget: String, @@ -52,6 +53,7 @@ fun NetworkDeviceHardware.asEntity() = DeviceHardwareEntity( hwModel = hwModel, hwModelSlug = hwModelSlug, images = images, + isMaker = isMaker, lastUpdated = nowMillis, partitionScheme = partitionScheme, platformioTarget = platformioTarget, @@ -69,6 +71,7 @@ fun DeviceHardwareEntity.asExternalModel() = DeviceHardware( hwModel = hwModel, hwModelSlug = hwModelSlug, images = images, + isMaker = isMaker, partitionScheme = partitionScheme, platformioTarget = platformioTarget, requiresDfu = requiresDfu, diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt index 6a439f6bf6..6f675b1cce 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt @@ -46,7 +46,8 @@ data class DiscoveredNodeEntity( @ColumnInfo(name = "longitude") val longitude: Double? = null, @ColumnInfo(name = "distance_from_user") val distanceFromUser: Double? = null, @ColumnInfo(name = "hop_count", defaultValue = "0") val hopCount: Int = 0, - @ColumnInfo(name = "snr", defaultValue = "0") val snr: Float = 0f, + /** Null when no packet from this node reported an snr. Rows written before schema 64 store 0 for both cases. */ + @ColumnInfo(name = "snr") val snr: Float? = null, /** Null when no packet from this node reported an rssi. Rows written before schema 51 store 0 for both cases. */ @ColumnInfo(name = "rssi") val rssi: Int? = null, @ColumnInfo(name = "message_count", defaultValue = "0") val messageCount: Int = 0, diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt index 0f8cbf52b8..cd1d04ae67 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt @@ -22,6 +22,8 @@ import androidx.room3.PrimaryKey import kotlinx.serialization.Serializable import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.DeviceVersion +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.core.model.NetworkFirmwareRelease @Serializable @@ -56,25 +58,4 @@ fun FirmwareReleaseEntity.asExternalModel() = FirmwareRelease( releaseType = releaseType, ) -data class FirmwareRelease( - val id: String = "", - val pageUrl: String = "", - val releaseNotes: String = "", - val title: String = "", - val zipUrl: String = "", - val lastUpdated: Long = nowMillis, - val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE, -) - fun FirmwareReleaseEntity.asDeviceVersion(): DeviceVersion = DeviceVersion(id.substringBeforeLast(".").replace("v", "")) - -fun FirmwareRelease.asDeviceVersion(): DeviceVersion = DeviceVersion(id.substringBeforeLast(".").replace("v", "")) - -enum class FirmwareReleaseType { - STABLE, - ALPHA, - - /** Nightly preview from the nightly host's root; gated behind the modules unlock. */ - NIGHTLY, - LOCAL, -} diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt index b0d9cbcabf..3da300a755 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.database.entity import androidx.room3.ColumnInfo +import androidx.room3.Embedded import androidx.room3.Entity import androidx.room3.Index import androidx.room3.PrimaryKey @@ -43,7 +44,10 @@ import org.meshtastic.core.model.MeshLog as ExternalMeshLog * @property fromRadio The decoded [FromRadio] protobuf object. */ @Suppress("EmptyCatchBlock", "SwallowedException", "ConstructorParameterNaming") -@Entity(tableName = "log", indices = [Index(value = ["from_num"]), Index(value = ["port_num"])]) +@Entity( + tableName = "log", + indices = [Index(value = ["from_num"]), Index(value = ["port_num"]), Index(value = ["received_date"])], +) data class MeshLog( @PrimaryKey val uuid: String, @ColumnInfo(name = "type") val message_type: String, @@ -85,6 +89,9 @@ data class MeshLog( } } +/** A [MeshLog] with its rowid, the cursor that continues a receive-order page read. */ +data class MeshLogRow(@ColumnInfo(name = "log_rowid") val rowId: Long, @Embedded val log: MeshLog) + fun MeshLog.asExternalModel() = ExternalMeshLog( uuid = uuid, message_type = message_type, diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index a34d1f95f3..833a34d403 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -41,74 +41,43 @@ data class NodeWithRelations( @Relation(entity = MetadataEntity::class, parentColumns = ["num"], entityColumns = ["num"]) val metadata: MetadataEntity?, ) { - // Direct construction avoids the previous `node.toModel().copy(metadata = …, manuallyVerified = …)` pattern, - // which allocated the Node twice per DB row (once from toModel, once from copy). Hot path on every DB emission. - fun toModel() = Node( - num = node.num, - user = node.user, - position = node.position, - snr = node.snr, - rssi = node.rssi, - lastHeard = node.lastHeard, - deviceMetrics = node.deviceMetrics ?: org.meshtastic.proto.DeviceMetrics.Builder().build(), - channel = node.channel, - viaMqtt = node.viaMqtt, - hopsAway = node.hopsAway, - isFavorite = node.isFavorite, - isIgnored = node.isIgnored, - isMuted = node.isMuted, - environmentMetrics = node.environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics.Builder().build(), - powerMetrics = node.powerMetrics ?: org.meshtastic.proto.PowerMetrics.Builder().build(), - airQualityMetrics = node.airQualityMetrics ?: org.meshtastic.proto.AirQualityMetrics.Builder().build(), - soilWaterMetrics = node.soilWaterMetrics ?: org.meshtastic.proto.SoilWaterMetrics.Builder().build(), - paxcounter = node.paxcounter, - publicKey = node.publicKey ?: node.user.public_key, - notes = node.notes, - powerChannelLabels = node.powerChannelLabels, - nodeStatus = node.nodeStatus, - lastTransport = node.lastTransport, - metadata = metadata?.proto, - manuallyVerified = node.manuallyVerified, - signsPackets = node.signsPackets, - heardOnCurrentLora = node.heardOnCurrentLora, - keyMatch = node.keyMatch, - newPublicKey = node.newPublicKey, - ) - - fun toEntity() = with(node) { - NodeEntity( - num = num, - user = user, - position = position, - snr = snr, - rssi = rssi, - lastHeard = lastHeard, - deviceTelemetry = deviceTelemetry, - channel = channel, - viaMqtt = viaMqtt, - hopsAway = hopsAway, - isFavorite = isFavorite, - isIgnored = isIgnored, - isMuted = isMuted, - environmentTelemetry = environmentTelemetry, - powerTelemetry = powerTelemetry, - airQualityTelemetry = airQualityTelemetry, - soilWaterTelemetry = soilWaterTelemetry, - paxcounter = paxcounter, - publicKey = publicKey ?: user.public_key, - notes = notes, - powerChannelLabels = powerChannelLabels, - manuallyVerified = manuallyVerified, - nodeStatus = nodeStatus, - lastTransport = lastTransport, - signsPackets = signsPackets, - heardOnCurrentLora = heardOnCurrentLora, - keyMatch = keyMatch, - newPublicKey = newPublicKey, - ) - } + fun toModel() = node.toModel(metadata?.proto) } +/** The one [Node] to [NodeEntity] mapping. [Node.metadata] lives in [MetadataEntity], so it is not carried here. */ +fun Node.toEntity() = NodeEntity( + num = num, + user = user, + position = position, + latitude = latitude, + longitude = longitude, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceTelemetry = Telemetry.Builder().also { wb -> wb.device_metrics = deviceMetrics }.build(), + channel = channel, + viaMqtt = viaMqtt, + hopsAway = hopsAway, + isFavorite = isFavorite, + isIgnored = isIgnored, + isMuted = isMuted, + environmentTelemetry = Telemetry.Builder().also { wb -> wb.environment_metrics = environmentMetrics }.build(), + powerTelemetry = Telemetry.Builder().also { wb -> wb.power_metrics = powerMetrics }.build(), + airQualityTelemetry = Telemetry.Builder().also { wb -> wb.air_quality_metrics = airQualityMetrics }.build(), + soilWaterTelemetry = Telemetry.Builder().also { wb -> wb.soil_water_metrics = soilWaterMetrics }.build(), + paxcounter = paxcounter, + publicKey = publicKey, + notes = notes, + powerChannelLabels = powerChannelLabels, + manuallyVerified = manuallyVerified, + nodeStatus = nodeStatus, + lastTransport = lastTransport, + signsPackets = signsPackets, + heardOnCurrentLora = heardOnCurrentLora, + keyMatch = keyMatch, + newPublicKey = newPublicKey, +) + @Entity(tableName = "metadata", indices = [Index(value = ["num"])]) data class MetadataEntity( @PrimaryKey val num: Int, @@ -239,8 +208,10 @@ data class NodeEntity( fun currentTime() = nowSeconds.toInt() } - fun toModel() = Node( + /** The one [NodeEntity] to [Node] mapping. [metadata] comes from the node's [MetadataEntity] row, when joined. */ + fun toModel(metadata: DeviceMetadata? = null) = Node( num = num, + metadata = metadata, user = user, position = position, snr = snr, @@ -261,6 +232,7 @@ data class NodeEntity( publicKey = publicKey ?: user.public_key, notes = notes, powerChannelLabels = powerChannelLabels, + manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, lastTransport = lastTransport, signsPackets = signsPackets, diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt index 49f9592a8d..0850923ec2 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -73,6 +73,7 @@ data class PacketEntity( filtered = filtered, transportMechanism = data.transportMechanism, xeddsaSigned = data.xeddsaSigned, + ackProofStatus = data.ackProofStatus, translatedText = translatedText, showTranslated = showTranslated, ) @@ -143,31 +144,7 @@ data class Packet( @ColumnInfo(name = "message_text", defaultValue = "") val messageText: String = "", @ColumnInfo(name = "translated_text") val translatedText: String? = null, @ColumnInfo(name = "show_translated", defaultValue = "0") val showTranslated: Boolean = false, -) { - companion object { - const val RELAY_NODE_SUFFIX_MASK = 0xFF - - fun getRelayNode(relayNodeId: Int, nodes: List, ourNodeNum: Int?): Node? { - val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK - - val candidateRelayNodes = - nodes.filter { - it.num != ourNodeNum && - it.lastHeard != 0 && - (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix - } - - val closestRelayNode = - if (candidateRelayNodes.size == 1) { - candidateRelayNodes.first() - } else { - candidateRelayNodes.minByOrNull { it.hopsAway } - } - - return closestRelayNode - } - } -} +) @Suppress("ConstructorParameterNaming") @Entity(tableName = "contact_settings") @@ -217,6 +194,8 @@ data class ReactionEntity( @ColumnInfo(name = "to") val to: String? = null, @ColumnInfo(name = "channel", defaultValue = "0") val channel: Int = 0, @ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteString? = null, + @ColumnInfo(name = "xeddsa_signed", defaultValue = "0") val xeddsaSigned: Boolean = false, + @ColumnInfo(name = "ack_proof_status", defaultValue = "0") val ackProofStatus: Int = 0, ) suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node?): Reaction { @@ -237,6 +216,8 @@ suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node to = to, channel = channel, sfppHash = sfpp_hash, + xeddsaSigned = xeddsaSigned, + ackProofStatus = ackProofStatus, ) } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt index 6e51617fb2..59b548c37b 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.database.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.PrimaryKey +import org.meshtastic.core.model.QuickChatAction as QuickChatActionModel @Entity(tableName = "quick_chat") data class QuickChatAction( @@ -33,3 +34,27 @@ data class QuickChatAction( Instant, } } + +fun QuickChatAction.asExternalModel() = QuickChatActionModel( + uuid = uuid, + name = name, + message = message, + mode = + when (mode) { + QuickChatAction.Mode.Append -> QuickChatActionModel.Mode.Append + QuickChatAction.Mode.Instant -> QuickChatActionModel.Mode.Instant + }, + position = position, +) + +fun QuickChatActionModel.asEntity() = QuickChatAction( + uuid = uuid, + name = name, + message = message, + mode = + when (mode) { + QuickChatActionModel.Mode.Append -> QuickChatAction.Mode.Append + QuickChatActionModel.Mode.Instant -> QuickChatAction.Mode.Instant + }, + position = position, +) diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt index b18d9f73b7..48e5e3fedb 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt @@ -163,6 +163,9 @@ abstract class DatabaseManagerTestFixture { /** `limitedParallelism` on a test dispatcher would swap virtual-time scheduling for a real worker view. */ override fun createPoolLane(): CoroutineDispatcher = testDispatchers.io + /** In-memory databases never share a file, so no other connection can hold one's write lock. */ + override suspend fun isWriteLockHeldElsewhere(database: MeshtasticDatabase): Boolean = false + /** Drives the pool-recovery rate window without relying on wall-clock or virtual-time advancement. */ var recoveryClockMillis: Long = 0L diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonDiscoveryDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonDiscoveryDaoTest.kt index 4b0f6ca5d4..d3ab8863e0 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonDiscoveryDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonDiscoveryDaoTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.core.database.dao import kotlinx.coroutines.flow.first @@ -421,6 +419,23 @@ abstract class CommonDiscoveryDaoTest { assertEquals(setOf(10L, 20L), nums.toSet()) } + @Test + fun getDiscoveredNodesForPresetResults_returnsOnlyTheListedPresetsInInsertOrder() = runTest { + createDb() + val sessionId = dao.insertSession(testSession()) + val presetA = dao.insertPresetResult(testPresetResult(sessionId, presetName = "A")) + val presetB = dao.insertPresetResult(testPresetResult(sessionId, presetName = "B")) + val presetC = dao.insertPresetResult(testPresetResult(sessionId, presetName = "C")) + dao.insertDiscoveredNode(testNode(presetC, nodeNum = 30)) + dao.insertDiscoveredNode(testNode(presetA, nodeNum = 10)) + dao.insertDiscoveredNode(testNode(presetB, nodeNum = 20)) + dao.insertDiscoveredNode(testNode(presetA, nodeNum = 11)) + + val nodes = dao.getDiscoveredNodesForPresetResults(listOf(presetA, presetC)) + + assertEquals(listOf(30L, 10L, 11L), nodes.map { it.nodeNum }) + } + @Test fun getMaxDistance_returnsLargestDistance() = runTest { createDb() diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt index 5f4204c681..a7ded6fc51 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt @@ -152,7 +152,7 @@ abstract class CommonNodeInfoDaoTest { assertEquals(trusted, stored?.publicKey) assertEquals(trusted, stored?.user?.public_key) assertFalse(stored?.keyMatch ?: true) - assertEquals(substitute, stored?.newPublicKey) + assertEquals(substitute, stored.newPublicKey) } @Test @@ -207,7 +207,7 @@ abstract class CommonNodeInfoDaoTest { val stillFlagged = dao.getNodeByNum(1)?.node assertEquals(trusted, stillFlagged?.publicKey) assertFalse(stillFlagged?.keyMatch ?: true) - assertEquals(substitute, stillFlagged?.newPublicKey) + assertEquals(substitute, stillFlagged.newPublicKey) } @Test @@ -262,7 +262,7 @@ abstract class CommonNodeInfoDaoTest { val stored = dao.getNodeByNum(own)?.node assertEquals(after, stored?.publicKey) assertTrue(stored?.keyMatch ?: false) - assertEquals(null, stored?.newPublicKey) + assertEquals(null, stored.newPublicKey) } @Test @@ -299,7 +299,7 @@ abstract class CommonNodeInfoDaoTest { val remote = dao.getNodeByNum(1)?.node assertEquals(trusted, remote?.publicKey) assertTrue(remote?.keyMatch ?: false) - assertEquals(null, remote?.newPublicKey) + assertEquals(null, remote.newPublicKey) // Nor may the local link write it over the connected radio's real key. A key of its own, or the new-node // guard would read this upsert as node 1 claiming a second number and never insert it. @@ -447,7 +447,7 @@ abstract class CommonNodeInfoDaoTest { val stored = dao.getNodeByNum(own) assertEquals(real, stored?.node?.publicKey) assertFalse(stored?.node?.keyMatch ?: true) - assertTrue(stored!!.toModel().mismatchKey) + assertTrue(stored.toModel().mismatchKey) } @Test diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/MeshLogDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/MeshLogDaoTest.kt index d1434b0c86..832dedb0bf 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/MeshLogDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/MeshLogDaoTest.kt @@ -147,6 +147,39 @@ class MeshLogDaoTest { assertEquals(setOf(firstUuid, secondUuid), remaining.map { it.uuid }.toSet()) } + @Test + fun testDeleteOlderThanRemovesAtMostTheLimit() = runTest { + meshLogDao.insertIgnore(List(5) { logEntry("old-$it", time = 100L + it) }) + meshLogDao.insert(logEntry("new", time = 1_000)) + + assertEquals(3, meshLogDao.deleteOlderThan(cutoffTimestamp = 500, limit = 3)) + + val remaining = meshLogDao.getAllLogsSnapshot().map { it.uuid } + assertEquals(2, remaining.count { it.startsWith("old-") }) + assertTrue("new" in remaining) + } + + @Test + fun testDeleteOlderThanDrainedInBatchesRemovesExactlyTheRowsBeforeTheCutoff() = runTest { + val cutoff = 10_000L + val limit = 4 + val survivors = mutableSetOf() + // Interleaved so insertion order does not line up with the cutoff. + repeat(limit * 2 + 1) { + meshLogDao.insert(logEntry("old-$it", time = cutoff - 1 - it)) + meshLogDao.insert(logEntry("new-$it", time = cutoff + it)) + survivors += "new-$it" + } + + val deletedPerCall = mutableListOf() + do { + deletedPerCall += meshLogDao.deleteOlderThan(cutoff, limit) + } while (deletedPerCall.last() == limit) + + assertEquals(listOf(limit, limit, 1), deletedPerCall) + assertEquals(survivors, meshLogDao.getAllLogsSnapshot().map { it.uuid }.toSet()) + } + @Test fun testGetLogsSnapshotPageTraversesEqualTimestampsWithoutDuplicates() = runTest { meshLogDao.insert(logEntry("log-a", time = 300)) diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntityMakerTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntityMakerTest.kt new file mode 100644 index 0000000000..d78d7f9d50 --- /dev/null +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntityMakerTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.database.entity + +import kotlinx.serialization.json.Json +import org.meshtastic.core.model.HardwareSupportTier +import org.meshtastic.core.model.NetworkDeviceHardware +import org.meshtastic.core.model.supportTier +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DeviceHardwareEntityMakerTest { + + private val json = Json { ignoreUnknownKeys = true } + + // The registry's shape for the first maker board: flagship supportLevel, activelySupported false. + private val makerEntry = + """ + {"hwModel":148,"hwModelSlug":"AXIOMETA_GENESIS_MINI","platformioTarget":"axiometa-genesis-mini", + "architecture":"esp32-s3","displayName":"Axiometa Genesis Mini","supportLevel":1, + "activelySupported":false,"isMaker":true,"tags":["Axiometa"]} + """ + .trimIndent() + + private val communityEntry = + """ + {"hwModel":1,"hwModelSlug":"TLORA_V2","platformioTarget":"tlora-v2","architecture":"esp32", + "displayName":"TLora V2","supportLevel":3,"activelySupported":false} + """ + .trimIndent() + + @Test + fun `isMaker survives the network to entity to model round trip`() { + val model = json.decodeFromString(makerEntry).asEntity().asExternalModel() + + assertTrue(model.isMaker) + assertEquals(HardwareSupportTier.MAKER, model.supportTier) + } + + @Test + fun `an entry without the key reads as not maker`() { + val model = json.decodeFromString(communityEntry).asEntity().asExternalModel() + + assertFalse(model.isMaker) + assertEquals(HardwareSupportTier.COMMUNITY, model.supportTier) + } +} diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/NodeMappingTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/NodeMappingTest.kt new file mode 100644 index 0000000000..a3b8848bb3 --- /dev/null +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/NodeMappingTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.database.entity + +import okio.ByteString.Companion.toByteString +import org.meshtastic.core.model.Node +import org.meshtastic.proto.AirQualityMetrics +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.DeviceMetrics +import org.meshtastic.proto.EnvironmentMetrics +import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.Paxcount +import org.meshtastic.proto.Position +import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.SoilWaterMetrics +import org.meshtastic.proto.User +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** A [Node] with every constructor property set away from its default, so a dropped field changes equality. */ +internal fun fullyPopulatedNode(): Node { + val key = ByteArray(32) { (it + 1).toByte() }.toByteString() + val refusedKey = ByteArray(32) { (it + 101).toByte() }.toByteString() + return Node( + num = 0x1234ABCD, + metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.8.1.abcdef" + wb.hw_model = HardwareModel.HELTEC_V3 + } + .build(), + user = + User.Builder() + .also { wb -> + wb.id = "!1234abcd" + wb.long_name = "Fixture Node" + wb.short_name = "FXN" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.public_key = key + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 525_200_000 + wb.longitude_i = 134_050_000 + wb.altitude = 34 + wb.time = 1_700_000_000 + } + .build(), + snr = 5.5f, + rssi = -91, + lastHeard = 1_700_000_100, + deviceMetrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.01f + } + .build(), + channel = 2, + viaMqtt = true, + hopsAway = 3, + isFavorite = true, + isIgnored = true, + isMuted = true, + environmentMetrics = EnvironmentMetrics.Builder().also { wb -> wb.temperature = 21.5f }.build(), + powerMetrics = PowerMetrics.Builder().also { wb -> wb.ch1_voltage = 12.6f }.build(), + airQualityMetrics = AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build(), + soilWaterMetrics = SoilWaterMetrics.Builder().also { wb -> wb.soil_ph = 6.8f }.build(), + paxcounter = Paxcount.Builder().also { wb -> wb.ble = 3 }.build(), + publicKey = key, + notes = "fixture notes", + powerChannelLabels = listOf("Solar", "Battery"), + manuallyVerified = true, + signsPackets = true, + heardOnCurrentLora = false, + nodeStatus = "on duty", + lastTransport = 1, + keyMatch = false, + newPublicKey = refusedKey, + ) +} + +class NodeMappingTest { + + @Test + fun `every node field survives a round trip through the entity`() { + val node = fullyPopulatedNode() + + assertEquals(node, node.toEntity().toModel(metadata = node.metadata)) + } + + @Test + fun `a joined row carries metadata and manual verification into the model`() { + val node = fullyPopulatedNode() + val row = + NodeWithRelations( + node = node.toEntity(), + metadata = MetadataEntity(num = node.num, proto = checkNotNull(node.metadata)), + ) + + assertEquals(node, row.toModel()) + } + + @Test + fun `an entity read without its metadata join keeps manual verification`() { + val entity = fullyPopulatedNode().toEntity() + + assertTrue(entity.toModel().manuallyVerified) + } + + @Test + fun `power channel labels reach the entity`() { + assertEquals(listOf("Solar", "Battery"), fullyPopulatedNode().toEntity().powerChannelLabels) + } +} diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/QuickChatActionMappingTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/QuickChatActionMappingTest.kt new file mode 100644 index 0000000000..94938c2274 --- /dev/null +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/QuickChatActionMappingTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.database.entity + +import kotlin.test.Test +import kotlin.test.assertEquals +import org.meshtastic.core.model.QuickChatAction as QuickChatActionModel + +class QuickChatActionMappingTest { + + @Test + fun `each mode maps to the entity mode of the same name`() { + QuickChatActionModel.Mode.entries.forEach { mode -> + val model = QuickChatActionModel(uuid = 7L, name = "Greeting", message = "Hello", mode = mode, position = 3) + + assertEquals( + QuickChatAction( + uuid = 7L, + name = "Greeting", + message = "Hello", + mode = QuickChatAction.Mode.valueOf(mode.name), + position = 3, + ), + model.asEntity(), + ) + } + } + + @Test + fun `an entity read back maps to the model it was written from`() { + QuickChatActionModel.Mode.entries.forEach { mode -> + val model = QuickChatActionModel(uuid = 7L, name = "Greeting", message = "Hello", mode = mode, position = 3) + + assertEquals(model, model.asEntity().asExternalModel()) + } + } +} diff --git a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/DatabaseManagerAbandonedWriterJvmTest.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/DatabaseManagerAbandonedWriterJvmTest.kt new file mode 100644 index 0000000000..c31efc348e --- /dev/null +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/DatabaseManagerAbandonedWriterJvmTest.kt @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.database + +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.room3.Room +import androidx.room3.RoomDatabase +import androidx.room3.exclusiveTransaction +import androidx.room3.immediateTransaction +import androidx.room3.useWriterConnection +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import okio.FileSystem +import okio.Path +import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon +import org.meshtastic.core.database.di.DatabaseDataStore +import org.meshtastic.core.database.di.asDatabaseDataStore +import org.meshtastic.core.database.entity.MyNodeEntity +import org.meshtastic.core.di.CoroutineDispatchers +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.uuid.Uuid + +/** + * Wedge recovery against real database files, where an abandoned `withDb` block and a replacement pool share one SQLite + * file and therefore one write lock. + * + * Runs in real time: a lock held by a native connection is invisible to virtual time, so `runTest` would skip past the + * `withDb` deadline before the lock is even taken. + */ +class DatabaseManagerAbandonedWriterJvmTest { + + private lateinit var tmpDir: Path + private lateinit var dataStoreScope: CoroutineScope + private lateinit var manager: FileBackedManager + + @BeforeTest + fun setUp() { + tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "abandonedWriter-${Uuid.random()}" + FileSystem.SYSTEM.createDirectories(tmpDir) + dataStoreScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + val datastore = + PreferenceDataStoreFactory.createWithPath( + scope = dataStoreScope, + produceFile = { tmpDir / "test.preferences_pb" }, + ) + manager = + FileBackedManager( + datastore.asDatabaseDataStore(), + CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Default, default = Dispatchers.Default), + tmpDir, + ) + } + + @AfterTest + fun tearDown() = runBlocking { + manager.close() + dataStoreScope.cancel() + FileSystem.SYSTEM.deleteRecursively(tmpDir) + } + + /** + * The production shape: a long write (a large MeshLog `DELETE`) outlives the `withDb` deadline and keeps the file's + * write lock. Room's first DAO Flow on the active database then syncs its invalidation triggers under `BEGIN + * IMMEDIATE`, and SQLITE_BUSY there is uncaught. + */ + @Test + fun abandonedBlockHoldingTheWriteLockLeavesTheActiveDatabaseUsable() = runBlocking { + manager.switchActiveDatabase("addrA") + manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "seed")) } + val original = manager.currentDb.value + val lockHeld = CompletableDeferred() + val releaseLock = CompletableDeferred() + + manager.withDbTimeoutMillisForTest = SHORT_WITH_DB_TIMEOUT_MS + val abandoned = async { + runCatching { + manager.withDb { db -> + db.useWriterConnection { connection -> + connection.immediateTransaction { + lockHeld.complete(Unit) + releaseLock.await() + } + } + } + } + } + lockHeld.await() + assertIs(abandoned.await().exceptionOrNull()) + manager.withDbTimeoutMillisForTest = DatabaseManager.WITH_DB_TIMEOUT_MS + + val observed = async { runCatching { manager.currentDb.value.nodeInfoDao().getMyNodeInfo().first() } } + val write = async { + runCatching { manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "after")) } } + } + // Past the busy timeout, so anything that contends for the lock has already failed by the time it is released. + delay(HOLD_AFTER_ABANDON_MS) + releaseLock.complete(Unit) + + assertNull( + observed.await().exceptionOrNull(), + "a DAO Flow must not fail while the abandoned block holds the lock", + ) + assertNull(write.await().exceptionOrNull(), "a write must wait out the abandoned block, not fail") + assertEquals("after", manager.currentDb.value.nodeInfoDao().getMyNodeInfo().first()?.firmwareVersion) + assertTrue( + manager.currentDb.value === original, + "a pool whose write lock is held elsewhere is busy, not wedged, so it stays published", + ) + awaitWritersDrained() + } + + /** A block stuck before SQLite holds no lock, so recovery must still publish a replacement that can write. */ + @Test + fun abandonedBlockHoldingNoLockIsReplacedByAWritablePool() = runBlocking { + manager.switchActiveDatabase("addrA") + manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "seed")) } + val original = manager.currentDb.value + val blockStarted = CompletableDeferred() + val releaseBlock = CompletableDeferred() + + manager.withDbTimeoutMillisForTest = SHORT_WITH_DB_TIMEOUT_MS + val abandoned = async { + runCatching { + manager.withDb { + blockStarted.complete(Unit) + releaseBlock.await() + } + } + } + blockStarted.await() + assertIs(abandoned.await().exceptionOrNull()) + manager.withDbTimeoutMillisForTest = DatabaseManager.WITH_DB_TIMEOUT_MS + + val replacement = manager.currentDb.value + assertTrue(replacement !== original, "a block that holds no lock must still be replaced") + assertEquals( + ROOM_MIN_BUSY_TIMEOUT_MS, + busyTimeoutOf(replacement), + "the probe must hand the published replacement back with its busy timeout, not zero", + ) + manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "after")) } + assertEquals("after", replacement.nodeInfoDao().getMyNodeInfo().first()?.firmwareVersion) + + releaseBlock.complete(Unit) + awaitWritersDrained() + } + + /** + * Under a rollback journal a writer can hold EXCLUSIVE, which blocks readers as well, so the replacement's own open + * fails on the lock before the probe's transaction runs. That is the same verdict: the pool is busy, not wedged. + */ + @Test + fun abandonedBlockHoldingAnExclusiveLockKeepsThePoolAdmittingWrites() = runBlocking { + manager.journalMode = RoomDatabase.JournalMode.TRUNCATE + manager.switchActiveDatabase("addrA") + manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "seed")) } + val original = manager.currentDb.value + val lockHeld = CompletableDeferred() + val releaseLock = CompletableDeferred() + + manager.withDbTimeoutMillisForTest = SHORT_WITH_DB_TIMEOUT_MS + val abandoned = async { + runCatching { + manager.withDb { db -> + db.useWriterConnection { connection -> + connection.exclusiveTransaction { + lockHeld.complete(Unit) + releaseLock.await() + } + } + } + } + } + lockHeld.await() + assertIs(abandoned.await().exceptionOrNull()) + manager.withDbTimeoutMillisForTest = DatabaseManager.WITH_DB_TIMEOUT_MS + + val write = async { + runCatching { manager.withDb { it.nodeInfoDao().setMyNodeInfo(myNode(firmwareVersion = "after")) } } + } + releaseLock.complete(Unit) + + assertNull(write.await().exceptionOrNull(), "a busy pool must not be quarantined") + assertTrue(manager.currentDb.value === original, "a busy pool stays published") + assertEquals("after", manager.currentDb.value.nodeInfoDao().getMyNodeInfo().first()?.firmwareVersion) + awaitWritersDrained() + } + + /** Reads the busy timeout of [database]'s single connection, the one the write-lock probe ran on. */ + private suspend fun busyTimeoutOf(database: MeshtasticDatabase): Long = database.useWriterConnection { connection -> + connection.usePrepared("PRAGMA busy_timeout") { statement -> + statement.step() + statement.getLong(0) + } + } + + private suspend fun awaitWritersDrained() = + withTimeout(DRAIN_TIMEOUT_MS) { while (manager.debugWriterCounts() != (0 to 0)) delay(POLL_MS) } + + private class FileBackedManager( + datastore: DatabaseDataStore, + dispatchers: CoroutineDispatchers, + private val dir: Path, + ) : DatabaseManager(datastore, dispatchers) { + var withDbTimeoutMillisForTest: Long = DatabaseManager.WITH_DB_TIMEOUT_MS + + override val withDbTimeoutMillis: Long + get() = withDbTimeoutMillisForTest + + /** Room's own default when null. */ + var journalMode: RoomDatabase.JournalMode? = null + + override fun buildDatabase(dbName: String): MeshtasticDatabase { + val builder = + Room.databaseBuilder( + name = (dir / "$dbName.db").toString(), + factory = { MeshtasticDatabaseConstructor.initialize() }, + ) + .configureCommon() + .setDriver(BusyTimeoutSQLiteDriver(BundledSQLiteDriver(), ROOM_MIN_BUSY_TIMEOUT_MS)) + journalMode?.let { builder.setJournalMode(it) } + return builder.build() + } + + /** No-op: eviction, legacy cleanup and backfill would read the platform data directory. */ + override fun schedulePostSwitchMaintenance(dbName: String, db: MeshtasticDatabase) = Unit + } + + private fun myNode(firmwareVersion: String) = MyNodeEntity( + myNodeNum = 1, + model = null, + firmwareVersion = firmwareVersion, + couldUpdate = false, + shouldUpdate = false, + currentPacketId = 0L, + messageTimeoutMsec = 0, + minAppVersion = 0, + maxChannels = 0, + hasWifi = false, + ) + + private companion object { + /** Room raises any lower `busy_timeout` to this when it opens a connection. */ + const val ROOM_MIN_BUSY_TIMEOUT_MS = 3_000L + + /** Long enough for the abandoned block to take the lock first, even on a loaded machine. */ + const val SHORT_WITH_DB_TIMEOUT_MS = 3_000L + const val HOLD_AFTER_ABANDON_MS = ROOM_MIN_BUSY_TIMEOUT_MS + 2_000L + const val DRAIN_TIMEOUT_MS = 10_000L + const val POLL_MS = 20L + } +} diff --git a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt index 6096fcfd66..b00ec11dc6 100644 --- a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt @@ -64,8 +64,8 @@ class MeshtasticDatabaseMigrationTest { @Test fun migrateAll() = runTest { helper.createDatabase(EARLIEST_SCHEMA_VERSION).close() - // Every bump through 52 is an @AutoMigration; 52→53 is the manual FTS-rebuild migration. - helper.runMigrationsAndValidate(latestSchemaVersion(), listOf(MeshtasticDatabase.MIGRATION_52_53)).close() + // Every bump is an @AutoMigration except the manual 52→53 (FTS rebuild) and 63→64 (no log table rebuild). + helper.runMigrationsAndValidate(latestSchemaVersion(), MANUAL_MIGRATIONS).close() } /** @@ -427,6 +427,264 @@ class MeshtasticDatabaseMigrationTest { } } + /** + * 59→60 adds `contact_settings.display_name` and `channel_set.last_reconciled`. Per-conversation state and the + * stored channel set come through untouched. `display_name` arrives empty, which is what every live conversation + * holds, and `last_reconciled` arrives NULL, so the first reconcile captures the current set and moves nothing. + */ + @Test + fun displayNameAndReconcileBaselineAddedWithoutDisturbingConversationsOrChannels() = runTest { + helper.createDatabase(DISPLAY_NAME_FROM_VERSION).use { connection -> + connection.execSQL( + "INSERT INTO contact_settings (contact_key, muteUntil, last_read_message_uuid, " + + "last_read_message_timestamp, filtering_disabled, draft, pinned) " + + "VALUES ('0^all', 9999, 7, 5000, 1, 'half typed', 1)", + ) + connection.execSQL("INSERT INTO contact_settings (contact_key, muteUntil) VALUES ('0!abcdef01', 0)") + connection.execSQL("INSERT INTO channel_set (id, channel_set) VALUES (0, x'$STORED_CHANNEL_SET_HEX')") + } + + helper.runMigrationsAndValidate(DISPLAY_NAME_TO_VERSION, emptyList()).use { connection -> + val live = "FROM contact_settings WHERE contact_key = '0^all'" + assertEquals( + listOf("0!abcdef01", "0^all"), + queryColumn(connection, "SELECT contact_key FROM contact_settings ORDER BY contact_key"), + ) + assertEquals(listOf("9999"), queryColumn(connection, "SELECT muteUntil $live")) + assertEquals(listOf("7"), queryColumn(connection, "SELECT last_read_message_uuid $live")) + assertEquals(listOf("half typed"), queryColumn(connection, "SELECT draft $live")) + assertEquals(listOf("1"), queryColumn(connection, "SELECT pinned $live")) + assertEquals( + listOf("", ""), + queryColumn(connection, "SELECT display_name FROM contact_settings ORDER BY contact_key"), + ) + assertEquals(listOf("0"), queryColumn(connection, "SELECT id FROM channel_set")) + assertEquals( + listOf(STORED_CHANNEL_SET_HEX), + queryColumn(connection, "SELECT hex(channel_set) FROM channel_set"), + ) + assertEquals(listOf(null), queryColumn(connection, "SELECT last_reconciled FROM channel_set")) + } + } + + /** + * 60→61 adds `nodes.soil_water_metrics`. Existing nodes keep their data, and the new column arrives as an empty + * blob, never NULL: it decodes to an empty `Telemetry`, the same as a node that has never sent soil readings. + */ + @Test + fun soilWaterColumnAddedAsEmptyBlobWithoutDisturbingNodes() = runTest { + helper.createDatabase(SOIL_WATER_FROM_VERSION).use { connection -> + // Every NOT NULL column without a default in schema 60; the BLOBs are empty protos. + val columns = + "num, user, position, latitude, longitude, snr, rssi, last_heard, device_metrics, channel, " + + "via_mqtt, hops_away, is_favorite, environment_metrics, power_metrics, paxcounter" + connection.execSQL( + "INSERT INTO nodes ($columns, long_name, notes) VALUES " + + "(42, x'', x'', 0.0, 0.0, -6.25, -90, 1000, x'', 0, 0, 1, 1, x'', x'', x'', " + + "'Minnie Mouse', 'keep me')", + ) + connection.execSQL( + "INSERT INTO nodes ($columns, long_name) VALUES " + + "(43, x'', x'', 0.0, 0.0, 0.0, 0, 2000, x'', 0, 0, 2, 0, x'', x'', x'', 'Mickey')", + ) + } + + helper.runMigrationsAndValidate(SOIL_WATER_TO_VERSION, emptyList()).use { connection -> + assertEquals(listOf("42", "43"), queryColumn(connection, "SELECT num FROM nodes ORDER BY num")) + assertEquals(listOf("Minnie Mouse"), queryColumn(connection, "SELECT long_name FROM nodes WHERE num = 42")) + assertEquals(listOf("keep me"), queryColumn(connection, "SELECT notes FROM nodes WHERE num = 42")) + assertEquals(listOf("-6.25"), queryColumn(connection, "SELECT snr FROM nodes WHERE num = 42")) + assertEquals(listOf("-90"), queryColumn(connection, "SELECT rssi FROM nodes WHERE num = 42")) + assertEquals(listOf("1", "0"), queryColumn(connection, "SELECT is_favorite FROM nodes ORDER BY num")) + assertEquals( + listOf("blob", "blob"), + queryColumn(connection, "SELECT typeof(soil_water_metrics) FROM nodes ORDER BY num"), + ) + assertEquals( + listOf("0", "0"), + queryColumn(connection, "SELECT length(soil_water_metrics) FROM nodes ORDER BY num"), + ) + } + } + + /** + * 61→62 adds `device_hardware.is_maker`. [migrateAll] only proves the resulting schema validates from an empty + * database; this proves a cached registry row survives the addition with the fields the support badge and the + * firmware flow read, and that the new column arrives as 0 - an absent flag means "not maker", never NULL. + */ + @Test + fun isMakerColumnAddedWithoutDisturbingDeviceHardware() = runTest { + helper.createDatabase(IS_MAKER_FROM_VERSION).use { connection -> + connection.execSQL( + "INSERT INTO device_hardware (actively_supported, architecture, display_name, hwModel, " + + "hw_model_slug, last_updated, platformio_target, support_level, tags) " + + "VALUES (1, 'esp32-s3', 'Heltec V3', 43, 'HELTEC_V3', 1000, 'heltec-v3', 1, " + + "'[\"Heltec\"]')", + ) + connection.execSQL( + "INSERT INTO device_hardware (actively_supported, architecture, display_name, hwModel, " + + "hw_model_slug, last_updated, platformio_target, requires_dfu) " + + "VALUES (0, 'nrf52840', 'RAK4631', 9, 'RAK4631', 2000, 'rak4631', 1)", + ) + } + + helper.runMigrationsAndValidate( + IS_MAKER_TO_VERSION, + listOf(MeshtasticDatabase.MIGRATION_52_53), + ).use { connection -> + assertEquals( + listOf("heltec-v3", "rak4631"), + queryColumn(connection, "SELECT platformio_target FROM device_hardware ORDER BY platformio_target"), + ) + assertEquals( + listOf("1", "0"), + queryColumn(connection, "SELECT actively_supported FROM device_hardware ORDER BY platformio_target"), + ) + assertEquals( + listOf("1", null), + queryColumn(connection, "SELECT support_level FROM device_hardware ORDER BY platformio_target"), + ) + assertEquals( + listOf("[\"Heltec\"]", null), + queryColumn(connection, "SELECT tags FROM device_hardware ORDER BY platformio_target"), + ) + assertEquals( + listOf(null, "1"), + queryColumn(connection, "SELECT requires_dfu FROM device_hardware ORDER BY platformio_target"), + ) + // 0, never NULL - the resolver reads an absent flag as not maker. + assertEquals( + listOf("0", "0"), + queryColumn(connection, "SELECT is_maker FROM device_hardware ORDER BY platformio_target"), + ) + connection.execSQL("UPDATE device_hardware SET is_maker = 1 WHERE platformio_target = 'rak4631'") + assertEquals( + listOf("1"), + queryColumn(connection, "SELECT is_maker FROM device_hardware WHERE platformio_target = 'rak4631'"), + ) + } + } + + /** + * 62→63 adds `reactions.xeddsa_signed` and `reactions.ack_proof_status`. A stored reaction keeps its delivery + * state, and both columns arrive as 0: unsigned and ACK_PROOF_ABSENT, which is what every reaction before them was. + */ + @Test + fun reactionAuthenticityColumnsAddedWithoutDisturbingReactions() = runTest { + helper.createDatabase(REACTION_AUTH_FROM_VERSION).use { connection -> + connection.execSQL( + "INSERT INTO reactions (myNodeNum, reply_id, user_id, emoji, timestamp, packet_id, status, relays, " + + "`to`) VALUES (7, 1001, '!0000abcd', '👍', 5000, 2002, 3, 1, '!0000beef')", + ) + } + + helper.runMigrationsAndValidate( + REACTION_AUTH_TO_VERSION, + listOf(MeshtasticDatabase.MIGRATION_52_53), + ).use { connection -> + val row = "FROM reactions WHERE reply_id = 1001" + assertEquals(listOf("2002"), queryColumn(connection, "SELECT packet_id $row")) + assertEquals(listOf("3"), queryColumn(connection, "SELECT status $row")) + assertEquals(listOf("1"), queryColumn(connection, "SELECT relays $row")) + assertEquals(listOf("!0000beef"), queryColumn(connection, "SELECT `to` $row")) + assertEquals(listOf("0"), queryColumn(connection, "SELECT xeddsa_signed $row")) + assertEquals(listOf("0"), queryColumn(connection, "SELECT ack_proof_status $row")) + } + } + + /** + * [MeshtasticDatabase.MIGRATION_63_64] rebuilds only `discovered_node`, to make `snr` nullable, and adds the + * `log.received_date` index in place. Every discovered node survives with its values and its parent link, a stored + * 0 dB stays 0 because it may be a real reading, and NULL becomes storable. The `log` table is never copied: its + * rowids are unchanged and the `traceroute_node_position` row that cascades from it survives. The batched retention + * delete is served by the new index instead of a table scan. + */ + @Test + fun discoveredNodeSnrGoesNullableAndLogGainsDateIndexWithoutRebuildingLog() = runTest { + helper.createDatabase(SCHEMA_64_FROM_VERSION).use { connection -> + // Out-of-order rowids: a copy into a fresh table would renumber them. + connection.execSQL( + "INSERT INTO log (rowid, uuid, type, received_date, message, from_num, port_num) " + + "VALUES (40, 'log-a', 'Packet', 1000, 'first', 42, 3)", + ) + connection.execSQL( + "INSERT INTO log (rowid, uuid, type, received_date, message) " + + "VALUES (7, 'log-b', 'LogRecord', 2000, 'second')", + ) + connection.execSQL( + "INSERT INTO traceroute_node_position (log_uuid, request_id, node_num, position) " + + "VALUES ('log-a', 5, 77, x'0801')", + ) + connection.execSQL( + "INSERT INTO discovery_session (id, timestamp, presets_scanned, home_preset) " + + "VALUES (1, 3000, 'LONG_FAST', 'LONG_FAST')", + ) + connection.execSQL( + "INSERT INTO discovery_preset_result (id, session_id, preset_name) VALUES (1, 1, 'LONG_FAST')", + ) + connection.execSQL( + "INSERT INTO discovered_node (id, preset_result_id, node_num, neighbor_type, snr, rssi) " + + "VALUES (1, 1, 99, 'mesh', 0.0, NULL)", + ) + connection.execSQL( + "INSERT INTO discovered_node (id, preset_result_id, node_num, long_name, neighbor_type, snr, rssi, " + + "message_count) VALUES (2, 1, 100, 'Relay', 'direct', -7.5, -80, 3)", + ) + } + + helper.runMigrationsAndValidate( + SCHEMA_64_TO_VERSION, + listOf(MeshtasticDatabase.MIGRATION_63_64), + ).use { connection -> + val logs = "FROM log ORDER BY received_date" + assertEquals(listOf("40", "7"), queryColumn(connection, "SELECT rowid $logs")) + assertEquals(listOf("log-a", "log-b"), queryColumn(connection, "SELECT uuid $logs")) + assertEquals(listOf("Packet", "LogRecord"), queryColumn(connection, "SELECT type $logs")) + assertEquals(listOf("1000", "2000"), queryColumn(connection, "SELECT received_date $logs")) + assertEquals(listOf("first", "second"), queryColumn(connection, "SELECT message $logs")) + assertEquals(listOf("42", "0"), queryColumn(connection, "SELECT from_num $logs")) + assertEquals(listOf("3", "0"), queryColumn(connection, "SELECT port_num $logs")) + val positions = "FROM traceroute_node_position WHERE log_uuid = 'log-a'" + assertEquals(listOf("77"), queryColumn(connection, "SELECT node_num $positions")) + assertEquals(listOf("0801"), queryColumn(connection, "SELECT hex(position) $positions")) + assertTrue( + "index_log_received_date" in queryColumn(connection, "SELECT name FROM pragma_index_list('log')"), + ) + assertEquals( + listOf("received_date"), + queryColumn(connection, "SELECT name FROM pragma_index_info('index_log_received_date')"), + ) + val deletePlan = + queryPlan( + connection, + "DELETE FROM log WHERE rowid IN (SELECT rowid FROM log WHERE received_date < 1500 LIMIT 100)", + ) + assertTrue(deletePlan.any { "index_log_received_date" in it }, deletePlan.toString()) + + val byId = "FROM discovered_node ORDER BY id" + assertEquals(listOf("1", "2"), queryColumn(connection, "SELECT id $byId")) + assertEquals(listOf("99", "100"), queryColumn(connection, "SELECT node_num $byId")) + assertEquals(listOf("1", "1"), queryColumn(connection, "SELECT preset_result_id $byId")) + assertEquals(listOf("0.0", "-7.5"), queryColumn(connection, "SELECT snr $byId")) + assertEquals(listOf(null, "-80"), queryColumn(connection, "SELECT rssi $byId")) + assertEquals(listOf("mesh", "direct"), queryColumn(connection, "SELECT neighbor_type $byId")) + assertEquals(listOf(null, "Relay"), queryColumn(connection, "SELECT long_name $byId")) + assertEquals(listOf("0", "3"), queryColumn(connection, "SELECT message_count $byId")) + connection.execSQL("UPDATE discovered_node SET snr = NULL WHERE id = 1") + assertEquals(listOf(null), queryColumn(connection, "SELECT snr FROM discovered_node WHERE id = 1")) + } + } + + /** The `detail` column of `EXPLAIN QUERY PLAN` for [sql], one entry per plan step. */ + private fun queryPlan(connection: SQLiteConnection, sql: String): List = + connection.prepare("EXPLAIN QUERY PLAN $sql").use { statement -> + buildList { + while (statement.step()) { + add(statement.getText(QUERY_PLAN_DETAIL_COLUMN)) + } + } + } + private fun queryColumn(connection: SQLiteConnection, sql: String): List = connection.prepare(sql).use { statement -> buildList { @@ -457,7 +715,22 @@ class MeshtasticDatabaseMigrationTest { const val HEARD_ON_LORA_TO_VERSION = 58 const val KEY_MATCH_FROM_VERSION = 58 const val KEY_MATCH_TO_VERSION = 59 + const val DISPLAY_NAME_FROM_VERSION = 59 + const val DISPLAY_NAME_TO_VERSION = 60 + const val SOIL_WATER_FROM_VERSION = 60 + const val SOIL_WATER_TO_VERSION = 61 + const val IS_MAKER_FROM_VERSION = 61 + const val IS_MAKER_TO_VERSION = 62 + const val REACTION_AUTH_FROM_VERSION = 62 + const val REACTION_AUTH_TO_VERSION = 63 + const val SCHEMA_64_FROM_VERSION = 63 + const val SCHEMA_64_TO_VERSION = 64 + const val QUERY_PLAN_DETAIL_COLUMN = 3 + + /** Every hand-written migration, which a walk across 52→53 or 63→64 must be given. */ + val MANUAL_MIGRATIONS = listOf(MeshtasticDatabase.MIGRATION_52_53, MeshtasticDatabase.MIGRATION_63_64) const val PUBLIC_KEY_BYTES = 32 + const val STORED_CHANNEL_SET_HEX = "0A0612044D657368" /** Room's runtime FTS content-sync triggers, verbatim from the generated MeshtasticDatabase_Impl. */ val FTS_SYNC_TRIGGERS = diff --git a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/entity/NodeMappingFixtureTest.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/entity/NodeMappingFixtureTest.kt new file mode 100644 index 0000000000..a616faf6a5 --- /dev/null +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/entity/NodeMappingFixtureTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.database.entity + +import org.meshtastic.core.model.Node +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class NodeMappingFixtureTest { + + /** + * The round-trip test only proves a field survives if the fixture sets it. A data class exposes one `componentN` + * per constructor property, so this fails as soon as [Node] gains a property the fixture leaves at its default. + */ + @Test + fun `the round trip fixture sets every Node constructor property`() { + val components = + Node::class + .java + .methods + .filter { it.name.matches(COMPONENT) && it.parameterCount == 0 } + .sortedBy { it.name.removePrefix("component").toInt() } + val fixture = fullyPopulatedNode() + val defaults = Node(num = 0) + + assertTrue(components.isNotEmpty()) + val unset = components.filter { it.invoke(fixture) == it.invoke(defaults) }.map { it.name } + assertEquals(emptyList(), unset, "fixture leaves these Node properties at their defaults") + } + + private companion object { + val COMPONENT = Regex("component\\d+") + } +} diff --git a/core/datastore/README.md b/core/datastore/README.md index 6ef3490d28..8218559d9d 100644 --- a/core/datastore/README.md +++ b/core/datastore/README.md @@ -4,12 +4,11 @@ **Targets:** Android · JVM (Desktop) · iOS -The `:core:datastore` module manages structured, asynchronous data storage using **Jetpack DataStore**. It is primarily used for storing complex configuration objects like radio channel sets and local device configurations. +The `:core:datastore` module manages structured, asynchronous data storage using **Jetpack DataStore**. It is primarily used for storing complex configuration objects like the connected device's local and module configurations. ## Key Components ### 1. Data Sources -- **`ChannelSetDataSource`**: Manages the storage of radio channel configurations. - **`LocalConfigDataSource`** / **`ModuleConfigDataSource`**: Store the connected device's `LocalConfig` and `LocalModuleConfig` protos. - **`LocalStatsDataSource`**: Stores the latest local device statistics telemetry. - **`RecentAddressesDataSource`**: Stores a list of recently connected radio addresses (BLE/USB/TCP). diff --git a/core/datastore/detekt-baseline.xml b/core/datastore/detekt-baseline.xml deleted file mode 100644 index da5d9cabe6..0000000000 --- a/core/datastore/detekt-baseline.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - CyclomaticComplexMethod:ModuleConfigDataSource.kt:ModuleConfigDataSource$suspend fun setLocalModuleConfig - - diff --git a/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt index 55bba0abaf..2f0a3f7d89 100644 --- a/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt +++ b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt @@ -32,11 +32,9 @@ import okio.Path import okio.Path.Companion.toOkioPath import org.koin.core.annotation.Module import org.koin.core.annotation.Single -import org.meshtastic.core.datastore.serializer.ChannelSetSerializer import org.meshtastic.core.datastore.serializer.LocalConfigSerializer import org.meshtastic.core.datastore.serializer.LocalStatsSerializer import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer -import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.LocalStats @@ -83,18 +81,6 @@ class ModuleConfigDataStoreModule { .asCoreModuleConfigDataStore() } -@Module -class ChannelSetDataStoreModule { - @Single - fun provideChannelSetDataStore(context: Context, scope: DataStoreScope): CoreChannelSetDataStore = protoStore( - serializer = ChannelSetSerializer, - producePath = { context.dataStoreFile("channel_set.pb").toOkioPath() }, - produceNewData = { ChannelSet.Builder().build() }, - scope = scope, - ) - .asCoreChannelSetDataStore() -} - @Module class LocalStatsDataStoreModule { @Single @@ -125,7 +111,6 @@ private fun protoStore( PreferencesDataStoreModule::class, LocalConfigDataStoreModule::class, ModuleConfigDataStoreModule::class, - ChannelSetDataStoreModule::class, LocalStatsDataStoreModule::class, ], ) diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt index ce6f9d0497..78617ad44e 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt @@ -21,9 +21,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.datastore.di.CorePreferencesDataStore @Single @@ -37,13 +36,10 @@ open class BootloaderWarningDataSource(private val dataStore: CorePreferencesDat dataStore.data.map { preferences -> val jsonString = preferences[PreferencesKeys.DISMISSED_BOOTLOADER_ADDRESSES] ?: return@map emptySet() - runCatching { Json.decodeFromString>(jsonString).toSet() } + // The stored value is a list of device addresses, so the log names only the exception type. + safeCatching { DatastoreJson.decodeFromString>(jsonString).toSet() } .onFailure { e -> - if (e is IllegalArgumentException || e is SerializationException) { - Logger.w(e) { "Failed to parse dismissed bootloader warning addresses, resetting preference" } - } else { - Logger.w(e) { "Unexpected error while parsing dismissed bootloader warning addresses" } - } + Logger.w { "Ignoring unreadable dismissed bootloader warning addresses (${e::class.simpleName})" } } .getOrDefault(emptySet()) } @@ -58,7 +54,7 @@ open class BootloaderWarningDataSource(private val dataStore: CorePreferencesDat val updated = (current + address).toList() dataStore.edit { preferences -> - preferences[PreferencesKeys.DISMISSED_BOOTLOADER_ADDRESSES] = Json.encodeToString(updated) + preferences[PreferencesKeys.DISMISSED_BOOTLOADER_ADDRESSES] = DatastoreJson.encodeToString(updated) } } } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt deleted file mode 100644 index b911670c1e..0000000000 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.datastore - -import co.touchlab.kermit.Logger -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import okio.IOException -import org.koin.core.annotation.Single -import org.meshtastic.core.datastore.di.CoreChannelSetDataStore -import org.meshtastic.proto.Channel -import org.meshtastic.proto.ChannelSet -import org.meshtastic.proto.ChannelSettings -import org.meshtastic.proto.Config - -/** Class that handles saving and retrieving [ChannelSet] data. */ -@Single -class ChannelSetDataSource(private val channelSetStore: CoreChannelSetDataStore) { - val channelSetFlow: Flow = - channelSetStore.data.catch { exception -> - // dataStore.data throws an IOException when an error is encountered when reading data - if (exception is IOException) { - Logger.e { "Error reading DeviceConfig settings: ${exception.message}" } - emit(ChannelSet.Builder().build()) - } else { - throw exception - } - } - - suspend fun clearChannelSet() { - channelSetStore.updateData { ChannelSet.Builder().build() } - } - - /** Replaces all [ChannelSettings] in a single atomic operation. */ - suspend fun replaceAllSettings(settingsList: List) { - channelSetStore.updateData { it.newBuilder().also { wb -> wb.settings = settingsList }.build() } - } - - /** Updates the [ChannelSettings] list with the provided channel. */ - suspend fun updateChannelSettings(channel: Channel) { - if (channel.role == Channel.Role.DISABLED) return - channelSetStore.updateData { preference -> - val settings = preference.settings.toMutableList() - // Resize to fit channel - while (settings.size <= channel.index) { - settings.add(ChannelSettings.Builder().build()) - } - // use setSettings() to ensure settingsList and channel indexes match - settings[channel.index] = channel.settings ?: ChannelSettings.Builder().build() - preference.newBuilder().also { wb -> wb.settings = settings }.build() - } - } - - suspend fun setLoraConfig(config: Config.LoRaConfig) { - channelSetStore.updateData { it.newBuilder().also { wb -> wb.lora_config = config }.build() } - } -} diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DataStoreReadFailure.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DataStoreReadFailure.kt new file mode 100644 index 0000000000..f1f72f2b07 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DataStoreReadFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.datastore + +/** + * True for the failures a DataStore read reports: okio's `IOException` from the file, and DataStore's own `IOException` + * (the base of its `CorruptionException`) from the contents. Both alias `java.io.IOException` on the JVM, but on Native + * they are separate classes, so a check for one alone misses the other. + */ +internal fun Throwable.isDataStoreReadFailure(): Boolean = + this is okio.IOException || this is androidx.datastore.core.IOException diff --git a/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/BinaryLogFile.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DatastoreJson.kt similarity index 55% rename from core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/BinaryLogFile.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DatastoreJson.kt index 2109c2e369..e03f44a184 100644 --- a/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/BinaryLogFile.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/DatastoreJson.kt @@ -14,18 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.common.util +package org.meshtastic.core.datastore -import android.content.Context -import java.io.File -import java.io.FileOutputStream +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json /** - * A specialized [FileOutputStream] that writes data to a file in the application's external files directory. Primarily - * used for low-level protocol debugging and packet logging. - * - * @param context The context used to locate the external files directory. - * @param name The name of the log file. + * Json for the JSON strings this module stores, which hold user data such as device addresses. Exceptions omit the JSON + * input, though a message can still quote one token, so callers log only the exception type. Not injected from Koin: + * Json is sealed, so Mokkery could no longer mock a data source that takes one. */ -class BinaryLogFile(context: Context, name: String) : - FileOutputStream(File(context.getExternalFilesDir(null), name), true) +@OptIn(ExperimentalSerializationApi::class) +internal val DatastoreJson = Json { exceptionsWithDebugInfo = false } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSource.kt index b1496685d8..c7471b98cf 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSource.kt @@ -21,9 +21,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.datastore.di.CorePreferencesDataStore import org.meshtastic.core.datastore.model.PendingFirmwareRecovery @@ -42,20 +41,19 @@ open class FirmwareRecoveryDataSource(private val dataStore: CorePreferencesData open val pending: Flow = dataStore.data.map { preferences -> val jsonString = preferences[PreferencesKeys.PENDING_RECOVERY] ?: return@map null - runCatching { Json.decodeFromString(jsonString) } + // The stored record holds the device address and name, so the log names only the exception type. + safeCatching { DatastoreJson.decodeFromString(jsonString) } .onFailure { e -> - if (e is IllegalArgumentException || e is SerializationException) { - Logger.w(e) { "Failed to parse pending firmware recovery, clearing preference" } - } else { - Logger.w(e) { "Unexpected error parsing pending firmware recovery" } - } + Logger.w { "Ignoring unreadable pending firmware recovery (${e::class.simpleName})" } } .getOrNull() } /** Records [recovery] as the outstanding interrupted update, replacing any previous record. */ open suspend fun set(recovery: PendingFirmwareRecovery) { - dataStore.edit { preferences -> preferences[PreferencesKeys.PENDING_RECOVERY] = Json.encodeToString(recovery) } + dataStore.edit { preferences -> + preferences[PreferencesKeys.PENDING_RECOVERY] = DatastoreJson.encodeToString(recovery) + } } /** Clears the outstanding recovery record (update finished, or the device returned on its own). */ diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt index b58f24f44f..db8d63a05d 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt @@ -19,7 +19,6 @@ package org.meshtastic.core.datastore import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import okio.IOException import org.koin.core.annotation.Single import org.meshtastic.core.datastore.di.CoreLocalConfigDataStore import org.meshtastic.proto.Config @@ -31,7 +30,7 @@ class LocalConfigDataSource(private val localConfigStore: CoreLocalConfigDataSto val localConfigFlow: Flow = localConfigStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data - if (exception is IOException) { + if (exception.isDataStoreReadFailure()) { Logger.e { "Error reading LocalConfig settings: ${exception.message}" } emit(LocalConfig.Builder().build()) } else { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt index 4807c5f689..d880043ef8 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt @@ -19,7 +19,6 @@ package org.meshtastic.core.datastore import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import okio.IOException import org.koin.core.annotation.Single import org.meshtastic.core.datastore.di.CoreLocalStatsDataStore import org.meshtastic.proto.LocalStats @@ -38,7 +37,7 @@ interface LocalStatsDataSource { open class LocalStatsDataSourceImpl(private val localStatsStore: CoreLocalStatsDataStore) : LocalStatsDataSource { override val localStatsFlow: Flow = localStatsStore.data.catch { exception -> - if (exception is IOException) { + if (exception.isDataStoreReadFailure()) { Logger.e { "Error reading LocalStats: ${exception.message}" } emit(LocalStats.Builder().build()) } else { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt index 8352730667..e46dcf5a79 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt @@ -19,7 +19,6 @@ package org.meshtastic.core.datastore import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import okio.IOException import org.koin.core.annotation.Single import org.meshtastic.core.datastore.di.CoreModuleConfigDataStore import org.meshtastic.proto.LocalModuleConfig @@ -31,7 +30,7 @@ class ModuleConfigDataSource(private val moduleConfigStore: CoreModuleConfigData val moduleConfigFlow: Flow = moduleConfigStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data - if (exception is IOException) { + if (exception.isDataStoreReadFailure()) { Logger.e { "Error reading LocalModuleConfig settings: ${exception.message}" } emit(LocalModuleConfig.Builder().build()) } else { @@ -85,6 +84,9 @@ class ModuleConfigDataSource(private val moduleConfigStore: CoreModuleConfigData config.statusmessage != null -> current.newBuilder().also { wb -> wb.statusmessage = config.statusmessage }.build() + config.traffic_management != null -> + current.newBuilder().also { wb -> wb.traffic_management = config.traffic_management }.build() + config.tak != null -> current.newBuilder().also { wb -> wb.tak = config.tak }.build() config.mesh_beacon != null -> diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt index 3fb79f3c08..f92f0eaf62 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt @@ -22,18 +22,19 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive import org.koin.core.annotation.Single import org.meshtastic.core.datastore.di.CorePreferencesDataStore import org.meshtastic.core.datastore.model.RecentAddress +/** + * The stored addresses and names are user data, so no log line here carries the stored value or an exception message, + * which kotlinx.serialization can fill with the input it failed on. + */ @Single open class RecentAddressesDataSource(private val dataStore: CorePreferencesDataStore) { private object PreferencesKeys { @@ -45,14 +46,10 @@ open class RecentAddressesDataSource(private val dataStore: CorePreferencesDataS val jsonString = preferences[PreferencesKeys.RECENT_IP_ADDRESSES] if (jsonString != null) { try { - Json.decodeFromString>(jsonString) + DatastoreJson.decodeFromString>(jsonString) } catch (e: IllegalArgumentException) { - Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" } - // Fallback to legacy parsing - parseLegacyRecentAddresses(jsonString) - } catch (e: SerializationException) { - Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" } - // Fallback to legacy parsing + // SerializationException is an IllegalArgumentException. + Logger.w { "Could not parse recent addresses (${e::class.simpleName}), trying legacy format" } parseLegacyRecentAddresses(jsonString) } } else { @@ -60,19 +57,21 @@ open class RecentAddressesDataSource(private val dataStore: CorePreferencesDataS } } - private fun parseLegacyRecentAddresses(jsonAddresses: String): List { - val jsonArray = Json.parseToJsonElement(jsonAddresses).jsonArray - return jsonArray.mapNotNull(::parseLegacyRecentAddress) + private fun parseLegacyRecentAddresses(jsonAddresses: String): List = try { + DatastoreJson.parseToJsonElement(jsonAddresses).jsonArray.mapNotNull(::parseLegacyRecentAddress) + } catch (e: IllegalArgumentException) { + Logger.w { "Discarding unreadable recent addresses (${e::class.simpleName})" } + emptyList() } private fun parseLegacyRecentAddress(item: kotlinx.serialization.json.JsonElement): RecentAddress? = when (item) { is JsonObject -> { - val address = item["address"]?.jsonPrimitive?.contentOrNull - val name = item["name"]?.jsonPrimitive?.contentOrNull + val address = (item["address"] as? JsonPrimitive)?.contentOrNull + val name = (item["name"] as? JsonPrimitive)?.contentOrNull if (address != null && name != null) { RecentAddress(address = address, name = name) } else { - Logger.w { "Skipping malformed recent address object: $item" } + Logger.w { "Skipping malformed recent address object" } null } } @@ -82,20 +81,20 @@ open class RecentAddressesDataSource(private val dataStore: CorePreferencesDataS if (address != null) { RecentAddress(address = address, name = "Meshtastic") } else { - Logger.w { "Skipping malformed recent address primitive: $item" } + Logger.w { "Skipping malformed recent address primitive" } null } } is JsonArray -> { - Logger.w { "Skipping nested array in recent IP addresses: $item" } + Logger.w { "Skipping nested array in recent IP addresses" } null } } open suspend fun setRecentAddresses(addresses: List) { dataStore.edit { preferences -> - preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = Json.encodeToString(addresses) + preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = DatastoreJson.encodeToString(addresses) } } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDataStores.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDataStores.kt index 86af5e698e..06b6da9c99 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDataStores.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDataStores.kt @@ -19,7 +19,6 @@ package org.meshtastic.core.datastore.di import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope -import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.LocalStats @@ -42,12 +41,6 @@ interface CorePreferencesDataStore : DataStore fun DataStore.asCorePreferencesDataStore(): CorePreferencesDataStore = object : CorePreferencesDataStore, DataStore by this {} -interface CoreChannelSetDataStore : DataStore - -/** Presents an existing store as [CoreChannelSetDataStore]; the wrapper adds nothing but identity. */ -fun DataStore.asCoreChannelSetDataStore(): CoreChannelSetDataStore = - object : CoreChannelSetDataStore, DataStore by this {} - interface CoreLocalConfigDataStore : DataStore /** Presents an existing store as [CoreLocalConfigDataStore]; the wrapper adds nothing but identity. */ diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt deleted file mode 100644 index 5493c75651..0000000000 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.datastore.serializer - -import androidx.datastore.core.CorruptionException -import androidx.datastore.core.okio.OkioSerializer -import okio.BufferedSink -import okio.BufferedSource -import okio.IOException -import org.meshtastic.proto.ChannelSet - -/** Serializer for the [ChannelSet] object defined in apponly.proto. */ -object ChannelSetSerializer : OkioSerializer { - override val defaultValue: ChannelSet = ChannelSet.Builder().build() - - override suspend fun readFrom(source: BufferedSource): ChannelSet { - try { - return ChannelSet.ADAPTER.decode(source) - } catch (exception: IOException) { - throw CorruptionException("Cannot read proto.", exception) - } - } - - override suspend fun writeTo(t: ChannelSet, sink: BufferedSink) { - ChannelSet.ADAPTER.encode(sink, t) - } -} diff --git a/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSourceTest.kt b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSourceTest.kt new file mode 100644 index 0000000000..7158b045e1 --- /dev/null +++ b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSourceTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import okio.FileSystem +import okio.Path +import org.meshtastic.core.datastore.di.asCorePreferencesDataStore +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.uuid.Uuid + +class BootloaderWarningDataSourceTest { + private lateinit var tmpDir: Path + private lateinit var dataStore: DataStore + private lateinit var dataSource: BootloaderWarningDataSource + private lateinit var logs: CapturingLogWriter + + private val testScope = TestScope(UnconfinedTestDispatcher()) + + @BeforeTest + fun setup() { + tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "bootloaderWarningTest-${Uuid.random()}" + FileSystem.SYSTEM.createDirectories(tmpDir) + dataStore = + PreferenceDataStoreFactory.createWithPath( + scope = testScope, + produceFile = { tmpDir / "test.preferences_pb" }, + ) + dataSource = BootloaderWarningDataSource(dataStore.asCorePreferencesDataStore()) + logs = CapturingLogWriter.install() + } + + @AfterTest + fun tearDown() { + CapturingLogWriter.uninstall() + FileSystem.SYSTEM.deleteRecursively(tmpDir) + } + + @Test + fun `a dismissed address reads back as dismissed`() = testScope.runTest { + dataSource.dismiss("AA:BB:CC:00:00:01") + + assertTrue(dataSource.isDismissed("AA:BB:CC:00:00:01")) + assertFalse(dataSource.isDismissed("AA:BB:CC:00:00:02")) + } + + @Test + fun `unreadable stored value counts as not dismissed and is never logged`() = testScope.runTest { + dataStore.edit { it[stringPreferencesKey("dismissed-bootloader-addresses")] = """["AA:BB:CC:11:22:33",""" } + + assertFalse(dataSource.isDismissed("AA:BB:CC:11:22:33")) + logs.assertNotLogged("AA:BB:CC:11:22:33") + } + + @Test + fun `dismiss replaces an unreadable stored value with a readable one`() = testScope.runTest { + dataStore.edit { it[stringPreferencesKey("dismissed-bootloader-addresses")] = """["AA:BB:CC:11:22:33",""" } + + dataSource.dismiss("AA:BB:CC:00:00:01") + + assertTrue(dataSource.isDismissed("AA:BB:CC:00:00:01")) + } +} diff --git a/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/CapturingLogWriter.kt b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/CapturingLogWriter.kt new file mode 100644 index 0000000000..8a09d10f06 --- /dev/null +++ b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/CapturingLogWriter.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.datastore + +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.platformLogWriter +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Records every Kermit line, including an attached throwable's full text, so a test can assert what never reached a + * log. Install it in setup and call [uninstall] in teardown: it replaces the global writers. + */ +internal class CapturingLogWriter : LogWriter() { + private val entries = mutableListOf() + + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + entries += message + throwable?.let { entries += it.stackTraceToString() } + } + + fun assertNotLogged(vararg values: String) { + assertTrue(entries.isNotEmpty(), "Expected the failed parse to log a warning") + for (value in values) { + assertFalse(entries.any { value in it }, "'$value' leaked into logs: $entries") + } + } + + companion object { + fun install(): CapturingLogWriter = CapturingLogWriter().also { + Logger.setLogWriters(it) + Logger.setMinSeverity(Severity.Verbose) + } + + fun uninstall() { + Logger.setLogWriters(platformLogWriter()) + } + } +} diff --git a/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSourceTest.kt b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSourceTest.kt new file mode 100644 index 0000000000..caa866d2be --- /dev/null +++ b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/FirmwareRecoveryDataSourceTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import okio.FileSystem +import okio.Path +import org.meshtastic.core.datastore.di.asCorePreferencesDataStore +import org.meshtastic.core.datastore.model.PendingFirmwareRecovery +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.uuid.Uuid + +class FirmwareRecoveryDataSourceTest { + private lateinit var tmpDir: Path + private lateinit var dataStore: DataStore + private lateinit var dataSource: FirmwareRecoveryDataSource + private lateinit var logs: CapturingLogWriter + + private val testScope = TestScope(UnconfinedTestDispatcher()) + + @BeforeTest + fun setup() { + tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "firmwareRecoveryTest-${Uuid.random()}" + FileSystem.SYSTEM.createDirectories(tmpDir) + dataStore = + PreferenceDataStoreFactory.createWithPath( + scope = testScope, + produceFile = { tmpDir / "test.preferences_pb" }, + ) + dataSource = FirmwareRecoveryDataSource(dataStore.asCorePreferencesDataStore()) + logs = CapturingLogWriter.install() + } + + @AfterTest + fun tearDown() { + CapturingLogWriter.uninstall() + FileSystem.SYSTEM.deleteRecursively(tmpDir) + } + + @Test + fun `a recorded recovery reads back until cleared`() = testScope.runTest { + val recovery = + PendingFirmwareRecovery( + fullAddress = "xAA:BB:CC:00:00:01", + hwModel = 9, + pioEnv = "rak4631", + releaseType = "STABLE", + deviceName = "Base", + ) + + dataSource.set(recovery) + assertEquals(recovery, dataSource.pending.first()) + + dataSource.clear() + assertNull(dataSource.pending.first()) + } + + @Test + fun `unreadable stored record reads as null without logging it`() = testScope.runTest { + val truncated = """{"fullAddress":"xAA:BB:CC:11:22:33","deviceName":"CabinBase","hwModel":""" + dataStore.edit { it[stringPreferencesKey("pending-firmware-recovery")] = truncated } + + assertNull(dataSource.pending.first()) + logs.assertNotLogged("AA:BB:CC:11:22:33", "CabinBase") + } +} diff --git a/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSourceTest.kt b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSourceTest.kt index 7584f2a93f..0bfc5d8f21 100644 --- a/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSourceTest.kt +++ b/core/datastore/src/commonTest/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSourceTest.kt @@ -16,20 +16,15 @@ */ package org.meshtastic.core.datastore +import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import kotlinx.coroutines.flow.Flow +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive import okio.FileSystem import okio.Path import org.meshtastic.core.datastore.di.asCorePreferencesDataStore @@ -44,7 +39,9 @@ import kotlin.uuid.Uuid class RecentAddressesDataSourceTest { private lateinit var tmpDir: Path + private lateinit var dataStore: DataStore private lateinit var dataSource: RecentAddressesDataSource + private lateinit var logs: CapturingLogWriter private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -53,19 +50,25 @@ class RecentAddressesDataSourceTest { fun setup() { tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "recentAddressesTest-${Uuid.random()}" FileSystem.SYSTEM.createDirectories(tmpDir) - val dataStore = + dataStore = PreferenceDataStoreFactory.createWithPath( scope = testScope, produceFile = { tmpDir / "test.preferences_pb" }, ) dataSource = RecentAddressesDataSource(dataStore.asCorePreferencesDataStore()) + logs = CapturingLogWriter.install() } @AfterTest fun tearDown() { + CapturingLogWriter.uninstall() FileSystem.SYSTEM.deleteRecursively(tmpDir) } + private suspend fun storeRaw(value: String) { + dataStore.edit { it[stringPreferencesKey("recent-ip-addresses")] = value } + } + // ---- recentAddresses flow ---- @Test @@ -97,6 +100,16 @@ class RecentAddressesDataSourceTest { assertEquals("5.6.7.8", result[0].address) } + @Test + fun `corrupt stored value yields an empty list without logging the stored addresses`() = testScope.runTest { + storeRaw("""[{"address":"10.20.30.40","name":"CabinRadio",""") + + val result = dataSource.recentAddresses.first() + + assertTrue(result.isEmpty()) + logs.assertNotLogged("10.20.30.40", "CabinRadio") + } + // ---- add() LRU behaviour ---- @Test @@ -186,13 +199,12 @@ class RecentAddressesDataSourceTest { assertTrue(dataSource.recentAddresses.first().isEmpty()) } - // ---- legacy JSON parsing (via LegacyParsingHarness) ---- + // ---- legacy stored formats ---- @Test fun `legacy JsonObject array is parsed correctly`() = testScope.runTest { - val legacyJson = - """[{"address":"192.168.1.100","name":"NodeA"},{"address":"192.168.1.101","name":"NodeB"}]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""[{"address":"192.168.1.100","name":"NodeA"},{"address":"192.168.1.101","name":"NodeB"}]""") + val result = dataSource.recentAddresses.first() assertEquals(2, result.size) assertEquals("192.168.1.100", result[0].address) @@ -204,8 +216,8 @@ class RecentAddressesDataSourceTest { @Test fun `legacy bare string JsonPrimitive array is parsed correctly`() = testScope.runTest { // Old clients stored plain IP strings with no name field - val legacyJson = """["192.168.1.50","10.0.0.2"]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""["192.168.1.50","10.0.0.2"]""") + val result = dataSource.recentAddresses.first() assertEquals(2, result.size) assertEquals("192.168.1.50", result[0].address) @@ -214,10 +226,19 @@ class RecentAddressesDataSourceTest { assertEquals("Meshtastic", result[1].name) } + @Test + fun `legacy value is parsed without logging the stored addresses`() = testScope.runTest { + storeRaw("""["192.168.1.50","10.0.0.2"]""") + + dataSource.recentAddresses.first() + + logs.assertNotLogged("192.168.1.50", "10.0.0.2") + } + @Test fun `legacy JsonObject missing address field is skipped`() = testScope.runTest { - val legacyJson = """[{"name":"NoAddress"},{"address":"1.2.3.4","name":"Good"}]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""[{"name":"NoAddress"},{"address":"1.2.3.4","name":"Good"}]""") + val result = dataSource.recentAddresses.first() assertEquals(1, result.size) assertEquals("1.2.3.4", result[0].address) @@ -225,63 +246,44 @@ class RecentAddressesDataSourceTest { @Test fun `legacy JsonObject missing name field is skipped`() = testScope.runTest { - val legacyJson = """[{"address":"1.2.3.4"},{"address":"5.6.7.8","name":"Good"}]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""[{"address":"1.2.3.4"},{"address":"5.6.7.8","name":"Good"}]""") + val result = dataSource.recentAddresses.first() assertEquals(1, result.size) assertEquals("5.6.7.8", result[0].address) } + @Test + fun `legacy JsonObject with non-primitive fields is skipped and keeps the other entries`() = testScope.runTest { + storeRaw( + """[{"address":{},"name":"BadA"},{"address":"9.9.9.9","name":["BadB"]},""" + + """{"address":"1.2.3.4","name":"Good"}]""", + ) + val result = dataSource.recentAddresses.first() + + assertEquals(listOf(RecentAddress("1.2.3.4", "Good")), result) + logs.assertNotLogged("BadA", "9.9.9.9", "BadB", "1.2.3.4", "Good") + } + @Test fun `legacy nested JsonArray entries are skipped`() = testScope.runTest { - val legacyJson = """[["nested","array"],{"address":"1.2.3.4","name":"Good"}]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""[["nested","array"],{"address":"1.2.3.4","name":"Good"}]""") + val result = dataSource.recentAddresses.first() assertEquals(1, result.size) assertEquals("1.2.3.4", result[0].address) } @Test - fun `legacy mixed array handles all element types`() = testScope.runTest { + fun `legacy mixed array handles all element types without logging entries`() = testScope.runTest { // JsonPrimitive + valid JsonObject + malformed JsonObject + nested JsonArray - val legacyJson = """["10.0.0.1",{"address":"10.0.0.2","name":"Node"},{"name":"bad"},[1,2]]""" - val result = LegacyParsingHarness(legacyJson).recentAddresses.first() + storeRaw("""["10.0.0.1",{"address":"10.0.0.2","name":"Node"},{"name":"BadEntryName"},["10.9.9.9"]]""") + val result = dataSource.recentAddresses.first() assertEquals(2, result.size) assertEquals("10.0.0.1", result[0].address) assertEquals("Meshtastic", result[0].name) assertEquals("10.0.0.2", result[1].address) - } -} - -/** - * Test harness that mirrors the private legacy parsing logic of [RecentAddressesDataSource] without needing to bypass - * encapsulation. Exposes a [Flow] that emits the result of parsing a raw legacy JSON string using the same rules as the - * production fallback path. - */ -private class LegacyParsingHarness(private val rawJson: String) { - val recentAddresses: Flow> = flow { - val jsonArray = Json.parseToJsonElement(rawJson).jsonArray - emit( - jsonArray.mapNotNull { item -> - when (item) { - is JsonObject -> { - val address = item["address"]?.jsonPrimitive?.contentOrNull - val name = item["name"]?.jsonPrimitive?.contentOrNull - if (address != null && name != null) { - RecentAddress(address = address, name = name) - } else { - null - } - } - - is JsonPrimitive -> { - item.contentOrNull?.let { RecentAddress(address = it, name = "Meshtastic") } - } - - is JsonArray -> null - } - }, - ) + logs.assertNotLogged("10.0.0.1", "10.0.0.2", "BadEntryName", "10.9.9.9") } } diff --git a/core/datastore/src/jvmTest/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSourceCoverageTest.kt b/core/datastore/src/jvmTest/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSourceCoverageTest.kt new file mode 100644 index 0000000000..11cb02af46 --- /dev/null +++ b/core/datastore/src/jvmTest/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSourceCoverageTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.datastore + +import com.squareup.wire.ProtoAdapter +import com.squareup.wire.WireField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.datastore.di.CoreModuleConfigDataStore +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** Walks Wire's generated oneof, so a variant the proto grows fails here by name. JVM-only: reads `@WireField`. */ +class ModuleConfigDataSourceCoverageTest { + + private class InMemoryModuleConfigStore : CoreModuleConfigDataStore { + override val data = MutableStateFlow(LocalModuleConfig.Builder().build()) + + override suspend fun updateData(transform: suspend (t: LocalModuleConfig) -> LocalModuleConfig) = + transform(data.value).also { data.value = it } + } + + @Test + fun `every ModuleConfig variant is persisted into its LocalModuleConfig section`() = runTest { + val store = InMemoryModuleConfigStore() + val dataSource = ModuleConfigDataSource(store) + val variants = + ModuleConfig::class.java.declaredFields.filter { + it.getAnnotation(WireField::class.java)?.oneofName == "payload_variant" + } + assertTrue(variants.isNotEmpty(), "found no @WireField fields in the ModuleConfig payload_variant oneof") + assertTrue(variants.any { it.name == "mesh_beacon" }, "mesh_beacon is not among the oneof fields found") + + variants.forEach { field -> + val value = (field.type.getField("ADAPTER").get(null) as ProtoAdapter<*>).decode(ByteArray(0)) + dataSource.setLocalModuleConfig( + ModuleConfig.Builder().also { it.javaClass.getField(field.name).set(it, value) }.build(), + ) + } + + val stored = store.data.first() + val dropped = variants.map { it.name }.filter { LocalModuleConfig::class.java.getField(it).get(stored) == null } + assertEquals(emptyList(), dropped, "these variants are never persisted, or a later one cleared them") + } +} diff --git a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt index 0ad68db8ae..8468788eb8 100644 --- a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt +++ b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt @@ -25,6 +25,7 @@ import org.meshtastic.core.di.CoroutineDispatchers @Module class CoreDiModule { @Single + @Suppress("InjectDispatcher") // the injection point itself fun provideCoroutineDispatchers(): CoroutineDispatchers = CoroutineDispatchers(io = ioDispatcher, main = Dispatchers.Main, default = Dispatchers.Default) } diff --git a/core/domain/README.md b/core/domain/README.md index 596653c7f7..67bef1d9fc 100644 --- a/core/domain/README.md +++ b/core/domain/README.md @@ -90,8 +90,6 @@ core:domain ├── core:model (domain models) ├── org.meshtastic:protobufs (Meshtastic protobuf types, Maven) ├── core:common - ├── core:database - ├── core:datastore └── core:resources ``` @@ -110,8 +108,6 @@ graph TB :core:domain -.-> :core:repository :core:domain -.-> :core:model :core:domain -.-> :core:common - :core:domain -.-> :core:database - :core:domain -.-> :core:datastore :core:domain -.-> :core:resources :core:domain -.-> :core:testing diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ef45d55ff0..e259b4021d 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -30,8 +30,6 @@ kotlin { implementation(projects.core.model) implementation(libs.meshtastic.protobufs) implementation(projects.core.common) - implementation(projects.core.database) - implementation(projects.core.datastore) implementation(projects.core.resources) implementation(libs.kermit) diff --git a/core/domain/detekt-baseline.xml b/core/domain/detekt-baseline.xml new file mode 100644 index 0000000000..d2acddf434 --- /dev/null +++ b/core/domain/detekt-baseline.xml @@ -0,0 +1,10 @@ + + + + + AbstractClassCanBeInterface:ProcessRadioResponseUseCase.kt:RadioResponseResult$RadioResponseResult + NoNameShadowing:ExportDataUseCase.kt:ExportDataUseCase${ it != 0 } + UseOrEmpty:ExportDataUseCase.kt:ExportDataUseCase$nodes[proto.from]?.user?.long_name ?: "" + UseOrEmpty:ExportDataUseCase.kt:ExportDataUseCase$proto.relay_node.takeIf { it != 0 }?.let { it.toByte().toHexString() } ?: "" + + diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt index ecce6625f3..73948d3f75 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt @@ -48,6 +48,19 @@ constructor( return packetId } + /** + * Reboots an nRF52 radio into its DFU bootloader. + * + * @param destNum The node number to reboot. + * @return The packet ID of the request. + */ + open suspend fun rebootToDfu(destNum: Int, onRequestId: (Int) -> Unit = {}): Int { + val packetId = radioController.generatePacketId() + onRequestId(packetId) + radioController.rebootToDfu(destNum, packetId) + return packetId + } + /** * Shuts down the radio. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt index 977240e027..ab362b80e4 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.domain.usecase.settings -import kotlinx.coroutines.flow.first import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import okio.BufferedSink @@ -38,12 +37,6 @@ constructor( private val nodeRepository: NodeRepository, private val meshLogRepository: MeshLogRepository, ) { - companion object { - private const val BYTE_MASK = 0xFF - private const val HEX_PAD_WIDTH = 2 - private const val HEX_RADIX = 16 - } - /** * Writes all persisted packet data to the provided [BufferedSink]. * @@ -65,7 +58,7 @@ constructor( "\"date\",\"time\",\"from\",\"sender name\",\"sender lat\",\"sender long\",\"rx lat\",\"rx long\",\"rx elevation\",\"rx snr\",\"distance(m)\",\"hop limit\",\"hop start\",\"relay node\",\"payload\"\n", ) - meshLogRepository.getAllLogsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet -> + meshLogRepository.readAllLogsInReceiveOrder().collect { packet -> packet.nodeInfo?.let { nodeInfo -> positionToPos.invoke(nodeInfo.position)?.let { nodePositions[nodeInfo.num] = nodeInfo.position } } @@ -104,10 +97,10 @@ constructor( val rxSnr = rxSnrOrNull val dist = - if (senderPos == null || rxPos == null) { + if (senderPosition == null || rxPosition == null || senderPos == null || rxPos == null) { "" } else { - positionToMeter(Position(rxPosition!!), Position(senderPosition!!)).roundToInt().toString() + positionToMeter(Position(rxPosition), Position(senderPosition)).roundToInt().toString() } val hopLimit = proto.hop_limit @@ -116,10 +109,7 @@ constructor( // relay_node carries only the last byte of the relaying node's NodeNum (0 means unset). // Emit it as a hex byte so it can be matched against the tail of a node id (e.g. !a1b2c3d4 -> // "d4"). - val relayNode = - proto.relay_node - .takeIf { it != 0 } - ?.let { (it and BYTE_MASK).toString(HEX_RADIX).padStart(HEX_PAD_WIDTH, '0') } ?: "" + val relayNode = proto.relay_node.takeIf { it != 0 }?.let { it.toByte().toHexString() } ?: "" val decoded = proto.decoded val encrypted = proto.encrypted val payload = diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt index 9bc2415591..5cea6d86ae 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt @@ -177,7 +177,10 @@ constructor( } lmc.paxcounter?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.paxcounter = it }.build()) } lmc.statusmessage?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.statusmessage = it }.build()) } + // traffic_management is not installed: it has no settings screen, and a node built without the module exports + // position_min_interval_secs = 0, which would silently turn off this node's default-on position dedup. lmc.tak?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.tak = it }.build()) } + lmc.mesh_beacon?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.mesh_beacon = it }.build()) } } private suspend fun AdminEditScope.installChannelsAndLora( diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt index 614d69ea49..aa311b260b 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt @@ -112,30 +112,14 @@ open class ProcessRadioResponseUseCase { return processAdminMessage(parsed) } - private fun processAdminMessage(parsed: AdminMessage): RadioResponseResult = when { - parsed.get_device_metadata_response != null -> - RadioResponseResult.Metadata(parsed.get_device_metadata_response!!) - - parsed.get_channel_response != null -> RadioResponseResult.ChannelResponse(parsed.get_channel_response!!) - - parsed.get_owner_response != null -> RadioResponseResult.Owner(parsed.get_owner_response!!) - - parsed.get_config_response != null -> RadioResponseResult.ConfigResponse(parsed.get_config_response!!) - - parsed.get_module_config_response != null -> - RadioResponseResult.ModuleConfigResponse(parsed.get_module_config_response!!) - - parsed.get_canned_message_module_messages_response != null -> - RadioResponseResult.CannedMessages(parsed.get_canned_message_module_messages_response!!) - - parsed.get_ringtone_response != null -> RadioResponseResult.Ringtone(parsed.get_ringtone_response!!) - - parsed.get_device_connection_status_response != null -> - RadioResponseResult.ConnectionStatus(parsed.get_device_connection_status_response!!) - - else -> { - Logger.d { "No custom processing needed for $parsed" } - RadioResponseResult.Success - } - } + private fun processAdminMessage(parsed: AdminMessage): RadioResponseResult = + parsed.get_device_metadata_response?.let(RadioResponseResult::Metadata) + ?: parsed.get_channel_response?.let(RadioResponseResult::ChannelResponse) + ?: parsed.get_owner_response?.let(RadioResponseResult::Owner) + ?: parsed.get_config_response?.let(RadioResponseResult::ConfigResponse) + ?: parsed.get_module_config_response?.let(RadioResponseResult::ModuleConfigResponse) + ?: parsed.get_canned_message_module_messages_response?.let(RadioResponseResult::CannedMessages) + ?: parsed.get_ringtone_response?.let(RadioResponseResult::Ringtone) + ?: parsed.get_device_connection_status_response?.let(RadioResponseResult::ConnectionStatus) + ?: RadioResponseResult.Success.also { Logger.d { "No custom processing needed for $parsed" } } } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt index 34ce8ed761..82bc748cff 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt @@ -16,39 +16,57 @@ */ package org.meshtastic.core.domain.usecase.settings +import co.touchlab.kermit.Logger +import kotlinx.coroutines.launch import org.koin.core.annotation.Single +import org.meshtastic.core.common.di.ApplicationCoroutineScope +import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.repository.MeshLogPrefs import org.meshtastic.core.repository.MeshLogRepository -/** Use case for managing mesh log settings. */ +/** + * Use case for managing mesh log settings. + * + * The deletes a settings change triggers run on [applicationScope], not the caller's scope: a retention prune deletes + * in batches and stops at cancellation, so closing the screen that asked for it must not cut the pass short. + */ @Single open class SetMeshLogSettingsUseCase constructor( private val meshLogRepository: MeshLogRepository, private val meshLogPrefs: MeshLogPrefs, + private val applicationScope: ApplicationCoroutineScope, ) { /** - * Sets the retention period for mesh logs. + * Sets the retention period for mesh logs and prunes to it in the background. * * @param days The number of days to retain logs. */ - suspend fun setRetentionDays(days: Int) { + fun setRetentionDays(days: Int) { val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS) meshLogPrefs.setRetentionDays(clamped) - meshLogRepository.deleteLogsOlderThan(clamped) + launchDeletion("prune to the new retention") { meshLogRepository.deleteLogsOlderThan(clamped) } } /** - * Enables or disables mesh logging. + * Enables or disables mesh logging, clearing or pruning the stored logs in the background. * * @param enabled True to enable logging, false to disable. */ - suspend fun setLoggingEnabled(enabled: Boolean) { + fun setLoggingEnabled(enabled: Boolean) { meshLogPrefs.setLoggingEnabled(enabled) if (!enabled) { - meshLogRepository.deleteAll() + launchDeletion("clear the disabled log") { meshLogRepository.deleteAll() } } else { - meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) + launchDeletion("prune to retention") { + meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) + } + } + } + + private fun launchDeletion(action: String, block: suspend () -> Unit) { + applicationScope.launch { + safeCatching { block() }.onFailure { Logger.e(it) { "Mesh log settings failed to $action" } } } } } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt index f7d865118a..9756319e84 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt @@ -105,11 +105,7 @@ class EnsureRemoteAdminSessionUseCaseTest { val sessionManager = stubSessionManager(refreshFlow = refresh) val controller = mock(MockMode.autofill) // Simulate the radio responding by emitting on the refresh flow when the metadata request fires. - everySuspend { controller.refreshMetadata(any()) } calls - { - refresh.tryEmit(destNum) - Unit - } + everySuspend { controller.refreshMetadata(any()) } calls { refresh.tryEmit(destNum) } val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope()) @@ -167,11 +163,7 @@ class EnsureRemoteAdminSessionUseCaseTest { val sessionManager = stubSessionManager(refreshFlow = refresh) val controller = mock(MockMode.autofill) var dispatches = 0 - everySuspend { controller.refreshMetadata(any()) } calls - { - dispatches++ - Unit - } + everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ } val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope()) @@ -195,11 +187,7 @@ class EnsureRemoteAdminSessionUseCaseTest { val sessionManager = stubSessionManager(refreshFlow = refresh) val controller = mock(MockMode.autofill) var dispatches = 0 - everySuspend { controller.refreshMetadata(any()) } calls - { - dispatches++ - Unit - } + everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ } val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope()) @@ -224,11 +212,7 @@ class EnsureRemoteAdminSessionUseCaseTest { val sessionManager = stubSessionManager(refreshFlow = refresh) val controller = mock(MockMode.autofill) var dispatches = 0 - everySuspend { controller.refreshMetadata(any()) } calls - { - dispatches++ - Unit - } + everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ } val connectionState = MutableStateFlow(ConnectionState.Connected) val repository = mock(MockMode.autofill) every { repository.connectionState } returns connectionState diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt index 5f571067c8..813e70a96c 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt @@ -28,6 +28,7 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class ExportDataUseCaseTest { @@ -170,4 +171,44 @@ class ExportDataUseCaseTest { // relay_node defaults to 0 (unset) -> blank field before the payload assertTrue(output.contains("\"0\",\"\",\"Hello\"")) } + + @Test + fun `invoke writes one row per log in the order the repository reads them`() = runTest { + val senders = listOf(30, 10, 20, 50, 40) + meshLogRepository.setLogs( + senders.mapIndexed { index, from -> + MeshLog( + uuid = "$index", + message_type = "TEXT", + received_date = 1000000000L + index, + raw_message = "", + fromRadio = + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.rx_snr = 5.0f + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "Hi".encodeUtf8() + } + .build() + } + .build() + } + .build(), + ) + }, + ) + val buffer = Buffer() + + useCase(buffer, 1) + + val rows = buffer.readUtf8().lines().drop(1).filter { it.isNotEmpty() } + assertEquals(senders.map { it.toString() }, rows.map { it.split("\",\"")[2] }) + } } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt index bfefe1b32a..f3bc028e27 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt @@ -87,7 +87,7 @@ class InstallProfileUseCaseTest { } @Test - fun `invoke installs all sections of a full profile`() = runTest { + fun `invoke opens an edit session for a populated profile`() = runTest { val profile = DeviceProfile.Builder() .also { wb -> diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt index ba9529d933..16e5f8b421 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt @@ -20,6 +20,8 @@ import kotlinx.coroutines.test.runTest import org.meshtastic.core.model.Position import org.meshtastic.core.repository.RadioController import org.meshtastic.core.testing.FakeRadioController +import org.meshtastic.core.testing.FakeRadioController.AdminRequest +import org.meshtastic.proto.Channel import org.meshtastic.proto.Config import org.meshtastic.proto.HamParameters import org.meshtastic.proto.ModuleConfig @@ -27,6 +29,8 @@ import org.meshtastic.proto.User import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue class RadioConfigUseCaseTest { @@ -36,9 +40,18 @@ class RadioConfigUseCaseTest { @BeforeTest fun setUp() { radioController = FakeRadioController() + radioController.nextPacketId = PACKET_ID useCase = RadioConfigUseCase(radioController) } + /** Asserts [call] sent exactly [expected] and, for requests that carry one, returned the same packet ID. */ + private suspend fun assertSends(expected: AdminRequest, call: suspend () -> Int?) { + val returned = call() + + assertEquals(listOf(expected), radioController.adminRequests) + if (expected.packetId != null) assertEquals(expected.packetId, returned) + } + @Test fun `getConfig invokes onRequestId with the packet id before issuing the send`() = runTest { // Guards against the response/registration race: for the locally connected node the firmware @@ -59,77 +72,138 @@ class RadioConfigUseCaseTest { } @Test - fun `setOwner calls radioController`() = runTest { + fun `setOwner sends the user to the node`() = runTest { val user = User.Builder().also { wb -> wb.long_name = "New Name" }.build() - useCase.setOwner(1234, user) - // Verify call implicitly or by adding tracking to FakeRadioController if needed. - // FakeRadioController already has getPacketId returning 1. + + assertSends(AdminRequest("setOwner", DEST, user, PACKET_ID)) { useCase.setOwner(DEST, user) } } @Test - fun `setHamMode calls radioController and returns packetId`() = runTest { - val packetId = - useCase.setHamMode( - 1234, - HamParameters.Builder() - .also { wb -> - wb.call_sign = "KK7ABC" - wb.short_name = "KK7A" - } - .build(), - ) - assertEquals(1, packetId) + fun `setHamMode sends the ham parameters to the node`() = runTest { + val ham = + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build() + + assertSends(AdminRequest("setHamMode", DEST, ham, PACKET_ID)) { useCase.setHamMode(DEST, ham) } } @Test - fun `getOwner calls radioController`() = runTest { - val packetId = useCase.getOwner(1234) - assertEquals(1, packetId) + fun `getOwner requests the owner`() = runTest { + assertSends(AdminRequest("getOwner", DEST, null, PACKET_ID)) { useCase.getOwner(DEST) } } @Test - fun `setConfig calls radioController`() = runTest { + fun `setConfig sends the config to the node`() = runTest { val config = Config.Builder() .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } .build() - useCase.setConfig(1234, config) + + assertSends(AdminRequest("setConfig", DEST, config, PACKET_ID)) { useCase.setConfig(DEST, config) } } @Test - fun `setModuleConfig calls radioController`() = runTest { + fun `setModuleConfig sends the module config to the node`() = runTest { val config = ModuleConfig.Builder() .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } .build() - useCase.setModuleConfig(1234, config) + + assertSends(AdminRequest("setModuleConfig", DEST, config, PACKET_ID)) { useCase.setModuleConfig(DEST, config) } } @Test - fun `setFixedPosition calls radioController`() = runTest { + fun `setFixedPosition sends the position unchanged`() = runTest { val position = Position(1.0, 2.0, 3) - useCase.setFixedPosition(1234, position) + + assertSends(AdminRequest("setFixedPosition", DEST, position, null)) { + useCase.setFixedPosition(DEST, position) + null + } } @Test - fun `removeFixedPosition calls radioController with zero position`() = runTest { useCase.removeFixedPosition(1234) } + fun `removeFixedPosition sends the removal sentinel`() = runTest { + useCase.removeFixedPosition(DEST) - @Test fun `setRingtone calls radioController`() = runTest { useCase.setRingtone(1234, "ringtone.mp3") } - - @Test fun `setCannedMessages calls radioController`() = runTest { useCase.setCannedMessages(1234, "messages") } - - @Test fun `getConfig calls radioController`() = runTest { useCase.getConfig(1234, 1) } - - @Test fun `getModuleConfig calls radioController`() = runTest { useCase.getModuleConfig(1234, 1) } - - @Test fun `getChannel calls radioController`() = runTest { useCase.getChannel(1234, 1) } - - @Test - fun `setRemoteChannel calls radioController`() = runTest { - useCase.setRemoteChannel(1234, org.meshtastic.proto.Channel.Builder().build()) + val request = radioController.adminRequests.single() + assertEquals(AdminRequest("setFixedPosition", DEST, request.payload, null), request) + assertTrue(assertIs(request.payload).isFixedPositionRemoval()) } - @Test fun `getRingtone calls radioController`() = runTest { useCase.getRingtone(1234) } + @Test + fun `setRingtone sends the ringtone`() = runTest { + assertSends(AdminRequest("setRingtone", DEST, "ringtone.mp3", null)) { + useCase.setRingtone(DEST, "ringtone.mp3") + null + } + } - @Test fun `getCannedMessages calls radioController`() = runTest { useCase.getCannedMessages(1234) } + @Test + fun `setCannedMessages sends the messages`() = runTest { + assertSends(AdminRequest("setCannedMessages", DEST, "messages", null)) { + useCase.setCannedMessages(DEST, "messages") + null + } + } + + @Test + fun `getConfig requests the config type`() = runTest { + assertSends(AdminRequest("getConfig", DEST, 1, PACKET_ID)) { useCase.getConfig(DEST, 1) } + } + + @Test + fun `getModuleConfig requests the module config type`() = runTest { + assertSends(AdminRequest("getModuleConfig", DEST, 1, PACKET_ID)) { useCase.getModuleConfig(DEST, 1) } + } + + @Test + fun `getChannel requests the channel index`() = runTest { + assertSends(AdminRequest("getChannel", DEST, 1, PACKET_ID)) { useCase.getChannel(DEST, 1) } + } + + @Test + fun `setRemoteChannel sends the channel to the node`() = runTest { + val channel = Channel.Builder().also { wb -> wb.index = 2 }.build() + + assertSends(AdminRequest("setRemoteChannel", DEST, channel, PACKET_ID)) { + useCase.setRemoteChannel(DEST, channel) + } + } + + @Test + fun `getRingtone requests the ringtone`() = runTest { + assertSends(AdminRequest("getRingtone", DEST, null, PACKET_ID)) { useCase.getRingtone(DEST) } + } + + @Test + fun `getCannedMessages requests the canned messages`() = runTest { + assertSends(AdminRequest("getCannedMessages", DEST, null, PACKET_ID)) { useCase.getCannedMessages(DEST) } + } + + @Test + fun `getDeviceConnectionStatus requests the connection status`() = runTest { + assertSends(AdminRequest("getDeviceConnectionStatus", DEST, null, PACKET_ID)) { + useCase.getDeviceConnectionStatus(DEST) + } + } + + @Test + fun `onRequestId receives the packet id the request carries`() = runTest { + var registered: Int? = null + + useCase.getOwner(DEST) { registered = it } + + assertEquals(PACKET_ID, registered) + assertEquals(PACKET_ID, radioController.adminRequests.single().packetId) + } + + private companion object { + const val DEST = 1234 + const val PACKET_ID = 4242 + } } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt index 6a30c60967..2c92779004 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt @@ -16,15 +16,25 @@ */ package org.meshtastic.core.domain.usecase.settings +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.MeshLog import org.meshtastic.core.repository.MeshLogRetention +import org.meshtastic.core.testing.FakeApplicationCoroutineScope import org.meshtastic.core.testing.FakeMeshLogPrefs import org.meshtastic.core.testing.FakeMeshLogRepository import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -39,7 +49,42 @@ class SetMeshLogSettingsUseCaseTest { fun setUp() { meshLogRepository = FakeMeshLogRepository() meshLogPrefs = FakeMeshLogPrefs() - useCase = SetMeshLogSettingsUseCase(meshLogRepository, meshLogPrefs) + useCase = + SetMeshLogSettingsUseCase( + meshLogRepository, + meshLogPrefs, + FakeApplicationCoroutineScope(UnconfinedTestDispatcher()), + ) + } + + @Test + fun `setRetentionDays prune keeps running after the caller is cancelled`() = runTest { + val now = nowMillis + meshLogRepository.setLogs( + listOf(MeshLog("recent", "TEXT", now, ""), MeshLog("stale", "TEXT", now - 8.days.inWholeMilliseconds, "")), + ) + val pruneStarted = CompletableDeferred() + val releasePrune = CompletableDeferred() + meshLogRepository.beforeDeleteLogsOlderThan = { + pruneStarted.complete(Unit) + releasePrune.await() + } + val appScopedUseCase = + SetMeshLogSettingsUseCase( + meshLogRepository, + meshLogPrefs, + FakeApplicationCoroutineScope(StandardTestDispatcher(testScheduler)), + ) + val caller = CoroutineScope(Job() + StandardTestDispatcher(testScheduler)) + + caller.launch { appScopedUseCase.setRetentionDays(7) } + runCurrent() + assertTrue(pruneStarted.isCompleted, "the prune must be in flight when the caller goes away") + caller.cancel() + releasePrune.complete(Unit) + runCurrent() + + assertEquals(listOf("recent"), meshLogRepository.currentLogs.map { it.uuid }) } @Test diff --git a/core/domain/src/jvmTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileModuleConfigCoverageTest.kt b/core/domain/src/jvmTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileModuleConfigCoverageTest.kt new file mode 100644 index 0000000000..2cf4a9f267 --- /dev/null +++ b/core/domain/src/jvmTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileModuleConfigCoverageTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.domain.usecase.settings + +import com.squareup.wire.Message +import com.squareup.wire.ProtoAdapter +import com.squareup.wire.WireField +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.testing.FakeRadioConfigRepository +import org.meshtastic.core.testing.FakeRadioController +import org.meshtastic.proto.DeviceProfile +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig +import java.lang.reflect.Field +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Walks Wire's generated fields, so a module config section the proto grows fails here by name until profile install + * writes it or [notInstalled] names it. JVM-only because it reads `@WireField`. + */ +class InstallProfileModuleConfigCoverageTest { + + private val notInstalled = + mapOf( + "traffic_management" to + "no settings screen, and a node built without the module exports its position dedup switched off", + ) + + @Test + fun `every section named as not installed is still a LocalModuleConfig section`() { + val stale = notInstalled.keys - localModuleConfigSections().map { it.name }.toSet() + + assertEquals(emptySet(), stale, "these sections left LocalModuleConfig, so drop them from notInstalled") + } + + @Test + fun `every LocalModuleConfig section has a ModuleConfig variant of the same name and type`() { + val variantTypes = moduleConfigVariants().associate { it.name to it.type } + + val unmatched = localModuleConfigSections().filter { variantTypes[it.name] != it.type }.map { it.name } + + assertEquals(emptyList(), unmatched, "these LocalModuleConfig sections have no matching ModuleConfig variant") + } + + @Test + fun `profile install writes every section except those named as not installed`() = runTest { + val radioController = FakeRadioController() + val sections = localModuleConfigSections() + val moduleConfig = + LocalModuleConfig.Builder() + .also { builder -> + sections.forEach { builder.javaClass.getField(it.name).set(builder, defaultOf(it)) } + } + .build() + + InstallProfileUseCase(radioController, FakeRadioConfigRepository())( + destNum = 1234, + profile = DeviceProfile.Builder().also { it.module_config = moduleConfig }.build(), + currentUser = null, + currentLoraConfig = null, + isLocal = false, + ) + + val written = radioController.allModuleConfigs.associate { it.onlyVariant() } + val installed = sections.filter { it.name !in notInstalled } + assertEquals( + emptyList(), + installed.map { it.name } - written.keys, + "profile install never writes these module config sections", + ) + assertEquals(emptySet(), notInstalled.keys intersect written.keys, "profile install writes these after all") + assertEquals(radioController.allModuleConfigs.size, written.size, "a section was written more than once") + installed.forEach { assertEquals(it.get(moduleConfig), written[it.name], "${it.name} was written changed") } + } + + private fun ModuleConfig.onlyVariant(): Pair = + moduleConfigVariants().mapNotNull { field -> field.get(this)?.let { field.name to it } }.single() + + private fun localModuleConfigSections(): List = LocalModuleConfig::class + .java + .declaredFields + .filter { it.isAnnotationPresent(WireField::class.java) && Message::class.java.isAssignableFrom(it.type) } + .also { sections -> + assertTrue(sections.isNotEmpty(), "found no @WireField message fields on LocalModuleConfig") + assertTrue(sections.any { it.name == "mesh_beacon" }, "mesh_beacon is not among the sections found") + } + + private fun moduleConfigVariants(): List = ModuleConfig::class + .java + .declaredFields + .filter { it.getAnnotation(WireField::class.java)?.oneofName == "payload_variant" } + .also { variants -> + assertTrue( + variants.isNotEmpty(), + "found no @WireField fields in the ModuleConfig payload_variant oneof", + ) + assertTrue(variants.any { it.name == "mesh_beacon" }, "mesh_beacon is not among the oneof fields found") + } + + private fun defaultOf(field: Field): Any? = + (field.type.getField("ADAPTER").get(null) as ProtoAdapter<*>).decode(ByteArray(0)) +} diff --git a/core/konsist/build.gradle.kts b/core/konsist/build.gradle.kts index e292aeee1c..9ee6f3611a 100644 --- a/core/konsist/build.gradle.kts +++ b/core/konsist/build.gradle.kts @@ -19,10 +19,7 @@ // scan every module's source from disk and assert the repo's KMP boundary rules. // Konsist is JVM-only, so its tests live in jvmTest (it cannot go in commonTest). // Runs under the existing `allTests` baseline gate via :core:konsist:allTests. -plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.jvm.android) -} +plugins { alias(libs.plugins.meshtastic.kmp.library) } kotlin { android { withHostTest {} } @@ -36,3 +33,17 @@ kotlin { } } } + +// Konsist reads every `.kt` in the checkout from disk, which Gradle cannot see. The patterns are anchored at the +// source roots because a leading `**` also claims the directories other tasks write, such as the docs sync targets. +tasks.named("jvmTest") { + inputs + .files( + fileTree(isolated.rootProject.projectDirectory) { + include("*/src/*/kotlin/**/*.kt", "*/*/src/*/kotlin/**/*.kt", "config/spotless/*.kt") + exclude("**/build/**") + }, + ) + .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName("konsistScannedSources") +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt index ac95f15ff4..29c6c9d47f 100644 --- a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.konsist import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.declaration.KoFileDeclaration import kotlin.test.Test import kotlin.test.assertTrue @@ -25,16 +26,28 @@ import kotlin.test.assertTrue * Datadog and Crashlytics on analytics the user is opted into by default. So an address must never be interpolated into * log or exception text raw — it goes through `Any?.anonymize()`, which keeps only a short suffix. * - * This is enforced as an architecture rule rather than by review because the failure mode is missing a site: a previous - * attempt anonymised the hand-written log statements in `core/ble` and missed the Kable `identifier`, which stamps the - * address onto *every* line the BLE library emits, plus further sites in the DFU transports and WiFi provisioning. + * This is an architecture rule rather than a review item because the failure mode is a missed site, and the easiest + * sites to miss reach the log indirectly: a Kable logging `identifier`, which stamps the address onto every line the + * BLE library emits, or a `tag` that a helper such as `retryBleOperation` prefixes to its own lines. * - * Scoped to the BLE-adjacent modules so matching on the `address` suffix stays low-noise. + * Scoped to the BLE-adjacent modules so matching on the `address` suffix stays low-noise. That scope includes the + * transport modules, so TCP hosts go through `anonymizePublicHost()`, which keeps a host on the user's own network + * readable. */ class BleAddressLoggingTest { private val scannedPathFragments = - listOf("/core/ble/", "/feature/firmware/", "/feature/wifi-provision/", "/feature/connections/") + listOf( + "/core/ble/", + "/core/network/", + "/core/service/", + "/feature/firmware/", + "/feature/wifi-provision/", + "/feature/connections/", + "/feature/discovery/", + "/androidApp/", + "/desktopApp/", + ) /** * Files where an address is used as an identity rather than as diagnostic text — building the connection string or @@ -42,8 +55,31 @@ class BleAddressLoggingTest { */ private val identityUseAllowlist = listOf("DeviceListEntry.kt") - /** Interpolation of anything ending in `address`, e.g. `${device.address}` or `$address`. */ - private val interpolatedAddress = Regex("""\$\{?[A-Za-z0-9_.]*[aA]ddress}?""") + /** + * Files whose `address` names hardware, not a person. The Android serial transport's address is the USB + * vendor-product pair (`usbSerialStableKey()`), which identifies the chip model, and the firmware retriever's are + * UF2 flash offsets. + */ + private val notPersonalAddressFiles = listOf("SerialRadioTransport.kt", "FirmwareRetriever.kt") + + /** + * Start of a call whose text reaches a log or a crash report. A `Logger.withTag(...)` prefix is part of the start. + */ + private val diagnosticCallStart = + Regex( + """Logger(\.withTag\([^)]*\))?\.\w+|\bthrow\s+\w+\s*\(|""" + + """\b(error|check|require|checkNotNull|requireNotNull|println)\s*\(""", + ) + + /** A string template entry, `${...}` or `$name`. */ + private val interpolation = Regex("""\$\{[^}]*}|\$[A-Za-z_]\w*""") + + private val addressReference = Regex("""[aA]ddress\b""") + + /** A named log-tag argument such as `tag = address` or Kable's `identifier = ...`, or a `withTag(...)` argument. */ + private val logTagArgument = Regex("""\b(tag|logTag|identifier)\s*=(?!=)\s*([^,)\n]*)|withTag\(([^)\n]*)\)""") + + private val stringLiteral = Regex("\"(?:\\\\.|[^\"\\\\])*\"") /** * Files this rule covers. @@ -56,28 +92,59 @@ class BleAddressLoggingTest { .filterNot { it.isNestedAgentWorktree() } .filter { file -> scannedPathFragments.any { it in file.scanPath } } .filterNot { file -> identityUseAllowlist.any { file.scanPath.endsWith(it) } } + .filterNot { file -> notPersonalAddressFiles.any { file.scanPath.endsWith(it) } } @Test fun `the scan actually reaches the BLE sources`() { val paths = scannedFiles().map { it.scanPath } assertTrue(paths.isNotEmpty(), emptyScanMessage("BLE-scoped scan")) - assertTrue( - paths.any { it.endsWith("KableBleConnection.kt") }, - "expected core/ble sources in scope; got ${paths.size} files, e.g. ${paths.take(3)}", - ) + val expected = + listOf( + "KableBleConnection.kt", + "BleRadioTransport.kt", + "SharedRadioInterfaceService.kt", + "DiscoveryScanEngine.kt", + ) + for (file in expected) { + assertTrue( + paths.any { it.endsWith(file) }, + "expected $file in scope; got ${paths.size} files, e.g. ${paths.take(3)}", + ) + } } @Test fun `a BLE address is never interpolated into log or exception text without anonymize`() { val offenders = scannedFiles().flatMap { file -> - file.text.lines().withIndex().mapNotNull { (index, line) -> - val isDiagnostic = "Logger." in line || "throw " in line || "check(" in line || "require(" in line - val interpolates = interpolatedAddress.containsMatchIn(line) - val anonymised = "anonymize" in line - if (isDiagnostic && interpolates && !anonymised) { - "${file.scanPath.substringAfterLast("/kotlin/")}:${index + 1}: " + line.trim() + diagnosticCallStart.findAll(file.text).flatMap { call -> + interpolation + .findAll(callText(file.text, call)) + .map { it.value } + .filter { addressReference.containsMatchIn(it) && "anonymize" !in it } + .map { "${file.location(call.range.first)}: ${call.value} ... $it" } + } + } + + assertTrue( + offenders.isEmpty(), + "BLE addresses must be anonymised in diagnostic text. Offending calls:\n" + offenders.joinToString("\n"), + ) + } + + @Test + fun `a BLE address is never used as a log tag without anonymize`() { + val offenders = + scannedFiles().flatMap { file -> + logTagArgument.findAll(file.text).mapNotNull { argument -> + val value = argument.groupValues[2].ifEmpty { argument.groupValues[3] } + val expression = + stringLiteral.replace(value) { literal -> + interpolation.findAll(literal.value).joinToString(" ") { it.value } + } + if (addressReference.containsMatchIn(expression) && "anonymize" !in expression) { + "${file.location(argument.range.first)}: ${argument.value.trim()}" } else { null } @@ -86,34 +153,43 @@ class BleAddressLoggingTest { assertTrue( offenders.isEmpty(), - "BLE addresses must be anonymised in diagnostic text. Offending lines:\n" + offenders.joinToString("\n"), + "A log tag or Kable logging identifier must be anonymised. Offending arguments:\n" + + offenders.joinToString("\n"), ) } - /** - * Kable stamps its `Logging.identifier` onto every line it emits, so passing a raw address there leaks it from - * library-internal logging that no per-call-site review would catch. - */ - @Test - fun `the Kable logging identifier is never a raw address`() { - val offenders = - Konsist.scopeFromProject() - .files - .filterNot { it.isNestedAgentWorktree() } - .filter { "/core/ble/" in it.scanPath } - .flatMap { file -> - file.text.lines().withIndex().mapNotNull { (index, line) -> - if ("identifier =" in line && "address" in line && "anonymize" !in line) { - "${file.scanPath.substringAfterLast("/kotlin/")}:${index + 1}: " + line.trim() - } else { - null - } - } - } - - assertTrue( - offenders.isEmpty(), - "Kable's logging identifier must be anonymised. Offending lines:\n" + offenders.joinToString("\n"), - ) + /** The text of the call starting at [start]: its argument list and its trailing lambda, each when present. */ + private fun callText(text: String, start: MatchResult): String { + var end = start.range.last + 1 + if (text[end - 1] == '(') { + end = closingIndex(text, end - 1) + 1 + } else { + val arguments = skipBlanks(text, end) + if (arguments < text.length && text[arguments] == '(') end = closingIndex(text, arguments) + 1 + } + val lambda = skipBlanks(text, end) + if (lambda < text.length && text[lambda] == '{') end = closingIndex(text, lambda) + 1 + return text.substring(start.range.first, end) } + + private fun skipBlanks(text: String, from: Int): Int { + var index = from + while (index < text.length && (text[index] == ' ' || text[index] == '\t')) index++ + return index + } + + /** Index of the bracket that closes the one at [open], or the last index of [text] when it never closes. */ + private fun closingIndex(text: String, open: Int): Int { + val openBracket = text[open] + val closeBracket = if (openBracket == '(') ')' else '}' + var depth = 0 + for (index in open until text.length) { + if (text[index] == openBracket) depth++ + if (text[index] == closeBracket && --depth == 0) return index + } + return text.lastIndex + } + + private fun KoFileDeclaration.location(offset: Int): String = + "${scanPath.substringAfterLast("/kotlin/")}:${text.take(offset).count { it == '\n' } + 1}" } diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CancellationExceptionSourceTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CancellationExceptionSourceTest.kt new file mode 100644 index 0000000000..ad95fdb3f9 --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CancellationExceptionSourceTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.Konsist +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Cancellation is caught and rethrown as `kotlinx.coroutines.CancellationException`, the name the constitution's + * operational standards require. The stdlib `kotlin.coroutines.cancellation` spelling is banned whether imported or + * written out in full, so the rule reads file text rather than imports. + */ +class CancellationExceptionSourceTest { + + // Assembled so this file does not match its own rule. + private val bannedName = listOf("kotlin", "coroutines", "cancellation", "CancellationException").joinToString(".") + + private val sourceFiles = Konsist.scopeFromProject().files.filterNot { it.isNestedAgentWorktree() } + + @Test + fun `the scan actually reaches project sources`() { + val paths = sourceFiles.map { it.scanPath } + + assertTrue(paths.isNotEmpty(), emptyScanMessage("project-wide scan")) + assertTrue( + paths.any { + it.endsWith("/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/Exceptions.kt") + }, + "expected core/common sources in scope; got ${paths.size} files, e.g. ${paths.take(3)}", + ) + } + + @Test + fun `no source names the stdlib CancellationException`() { + val offenders = sourceFiles.flatMap { file -> + file.text.lines().withIndex().mapNotNull { (index, line) -> + if (bannedName in line) "${file.scanPath}:${index + 1}: ${line.trim()}" else null + } + } + + assertTrue( + offenders.isEmpty(), + "Use kotlinx.coroutines.CancellationException instead of $bannedName:\n" + offenders.joinToString("\n"), + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CommonMainFrameworkBoundaryTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CommonMainFrameworkBoundaryTest.kt index e5d694b280..90026adfff 100644 --- a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CommonMainFrameworkBoundaryTest.kt +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CommonMainFrameworkBoundaryTest.kt @@ -28,7 +28,7 @@ import kotlin.test.assertTrue * the JVM (`java.*`) or Android (`android.*`) platform APIs — use the KMP equivalents (Okio, kotlinx-datetime, * atomicfu, Mutex, …) instead. * - * Konsist scans every module's Kotlin source from disk, so this single test covers all 37 modules. It runs on the JVM + * Konsist scans every module's Kotlin source from disk, so this single test covers every module. It runs on the JVM * (Konsist is JVM-only) under the existing `allTests` baseline gate. */ class CommonMainFrameworkBoundaryTest { diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoreDataManagerSeamTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoreDataManagerSeamTest.kt new file mode 100644 index 0000000000..ade08af38a --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoreDataManagerSeamTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.declaration.KoFileDeclaration +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * In `core:data`, the mesh engine (`manager`) and the storage side (`repository`, `datasource`) reach each other only + * through the `core:repository` interfaces that Koin binds, never by naming each other's packages. That keeps `manager` + * separable into its own module. Production code only; a test may wire both halves. + */ +class CoreDataManagerSeamTest { + + private val productionFiles = + Konsist.scopeFromProject().files.filterNot { it.isNestedAgentWorktree() }.filterNot { it.isTestSource } + + private val managerPackage = "org.meshtastic.core.data.manager" + + private val storagePackages = listOf("org.meshtastic.core.data.repository", "org.meshtastic.core.data.datasource") + + private fun KoFileDeclaration.isIn(packageName: String): Boolean = + packagee?.name?.let { it == packageName || it.startsWith("$packageName.") } == true + + private val managerFiles = productionFiles.filter { it.isIn(managerPackage) } + + private val storageFiles = productionFiles.filter { file -> storagePackages.any { file.isIn(it) } } + + /** Every line of [files] that names one of [packages], as an import or written out in full. */ + private fun references(files: List, packages: List): List { + val named = Regex(packages.joinToString("|") { Regex.escape(it) + """\b""" }) + return files.flatMap { file -> + file.text.lines().withIndex().mapNotNull { (index, line) -> + if (named.containsMatchIn(line)) "${file.scanPath}:${index + 1}: ${line.trim()}" else null + } + } + } + + @Test + fun `the scan actually reaches core data manager and storage sources`() { + assertTrue(managerFiles.isNotEmpty(), emptyScanMessage("core:data manager scan")) + assertTrue(storageFiles.isNotEmpty(), emptyScanMessage("core:data repository and datasource scan")) + assertTrue( + managerFiles.any { it.scanPath.endsWith("/core/data/manager/FromRadioPacketHandlerImpl.kt") }, + "expected core/data manager sources in scope; got ${managerFiles.size} files", + ) + assertTrue( + storageFiles.any { it.scanPath.endsWith("/core/data/repository/NodeRepositoryImpl.kt") }, + "expected core/data repository sources in scope; got ${storageFiles.size} files", + ) + } + + @Test + fun `manager never names repository or datasource`() { + val offenders = references(managerFiles, storagePackages) + + assertTrue( + offenders.isEmpty(), + "core.data.manager reaches storage through the core:repository interfaces, not core.data.repository or " + + "core.data.datasource. Offending lines:\n" + + offenders.joinToString("\n"), + ) + } + + @Test + fun `repository and datasource never name manager`() { + val offenders = references(storageFiles, listOf(managerPackage)) + + assertTrue( + offenders.isEmpty(), + "core.data.repository and core.data.datasource must not depend on core.data.manager; share through " + + "core:repository or core:model instead. Offending lines:\n" + + offenders.joinToString("\n"), + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoroutineScopeConstructionTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoroutineScopeConstructionTest.kt new file mode 100644 index 0000000000..6d987e29b1 --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/CoroutineScopeConstructionTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.declaration.KoFileDeclaration +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Production code takes its coroutine scope from an owner (`ApplicationCoroutineScope`, `ServiceScope`, a lifecycle + * scope or a constructor parameter) rather than calling `CoroutineScope(...)` itself: a self-made scope has no owner to + * cancel it, no exception handler, and nothing a test can swap in. The allowlist is the current set of sites, one entry + * per call, keyed `//`. It is pinned exactly, so a removed site comes off the list too. Test + * source sets and `core:testing` build scopes on purpose and are not scanned. + */ +class CoroutineScopeConstructionTest { + + private val allowed = + listOf( + "androidApp/google/AppFunctionStateSync.kt", + "androidApp/google/GoogleMapsPrefs.kt", + "androidApp/main/MeshUtilApplication.kt", + "core/data/commonMain/FromRadioPacketHandlerImpl.kt", + "core/data/commonMain/SingleFlightRefresher.kt", + "core/database/commonMain/DatabaseManager.kt", + "core/datastore/commonMain/CoreDatastoreModule.kt", + "core/network/androidMain/SerialRadioTransport.kt", + "core/network/commonMain/BleRadioTransport.kt", + "core/network/commonMain/BleRadioTransport.kt", + "core/network/commonMain/MQTTRepositoryImpl.kt", + "core/network/commonMain/MockRadioTransport.kt", + "core/network/commonMain/ReplayRadioTransport.kt", + "core/prefs/androidMain/CorePrefsAndroidModule.kt", + "core/prefs/commonMain/AnalyticsPrefsImpl.kt", + "core/prefs/commonMain/AppFunctionsPrefsImpl.kt", + "core/prefs/commonMain/CustomEmojiPrefsImpl.kt", + "core/prefs/commonMain/DiscoveryPrefsImpl.kt", + "core/prefs/commonMain/FilterPrefsImpl.kt", + "core/prefs/commonMain/HomoglyphPrefsImpl.kt", + "core/prefs/commonMain/MapConsentPrefsImpl.kt", + "core/prefs/commonMain/MapPrefsImpl.kt", + "core/prefs/commonMain/MapTileProviderPrefsImpl.kt", + "core/prefs/commonMain/MeshBeaconPrefsImpl.kt", + "core/prefs/commonMain/MeshLogPrefsImpl.kt", + "core/prefs/commonMain/MeshPrefsImpl.kt", + "core/prefs/commonMain/NotificationPrefsImpl.kt", + "core/prefs/commonMain/RadioPrefsImpl.kt", + "core/prefs/commonMain/TakPrefsImpl.kt", + "core/prefs/commonMain/UiPrefsImpl.kt", + "core/service/androidMain/BootCompleteReceiver.kt", + "core/service/androidMain/ConversationActionService.kt", + "core/service/androidMain/MeshService.kt", + "core/service/commonMain/CoreServiceModule.kt", + "core/service/commonMain/MeshServiceOrchestrator.kt", + "core/service/commonMain/MeshServiceOrchestrator.kt", + "core/service/commonMain/SharedRadioInterfaceService.kt", + "core/service/commonMain/SharedRadioInterfaceService.kt", + "core/takserver/jvmAndroidMain/TAKClientConnection.kt", + "desktopApp/main/DesktopMessageQueue.kt", + "desktopApp/main/DesktopPreferencesDataSource.kt", + "desktopApp/main/NoopStubs.kt", + "feature/discovery/commonMain/DiscoveryScanEngine.kt", + "feature/docs/commonMain/ChirpySessionHolder.kt", + "feature/firmware/commonMain/BleOtaTransport.kt", + "feature/firmware/commonMain/LegacyDfuTransport.kt", + "feature/firmware/commonMain/SecureDfuTransport.kt", + "feature/map-maplibre/commonMain/OfflineTerrainRepository.kt", + "feature/map/commonMain/CustomTileProviderRepository.kt", + "feature/map/commonMain/LayerOpacityStore.kt", + "feature/map/commonMain/MapLayersManager.kt", + "feature/widget/main/AndroidAppWidgetUpdater.kt", + "feature/widget/main/LocalStatsWidgetState.kt", + "feature/wifi-provision/commonMain/NymeaWifiService.kt", + ) + + /** A call to the `CoroutineScope(context)` factory; `rememberCoroutineScope()` has no word break before it. */ + private val construction = Regex("""\bCoroutineScope\(""") + + private val productionFiles = + Konsist.scopeFromProject() + .files + .filterNot { it.isNestedAgentWorktree() } + .filterNot { it.isTestSource } + .filterNot { it.scanPath.startsWith("/core/testing/") } + + private data class Site(val key: String, val location: String) + + private val KoFileDeclaration.allowlistKey: String + get() = "${scanPath.removePrefix("/").substringBefore("/src/")}/$sourceSet/${scanPath.substringAfterLast('/')}" + + private val sites: List = productionFiles.flatMap { file -> + file.text.lines().withIndex().mapNotNull { (index, line) -> + val code = line.trim() + val isComment = code.startsWith("//") || code.startsWith("*") || code.startsWith("/*") + if (!isComment && construction.containsMatchIn(code)) { + Site(file.allowlistKey, "${file.scanPath}:${index + 1}: $code") + } else { + null + } + } + } + + @Test + fun `the scan actually reaches production sources`() { + val paths = productionFiles.map { it.scanPath } + + assertTrue(paths.isNotEmpty(), emptyScanMessage("production source scan")) + assertTrue( + paths.any { it.endsWith("/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt") }, + "expected core/prefs commonMain sources in scope; got ${paths.size} files, e.g. ${paths.take(3)}", + ) + assertTrue(sites.isNotEmpty(), "found no CoroutineScope( call at all, so the pattern matches nothing") + } + + @Test + fun `no production code constructs a CoroutineScope outside the allowlist`() { + val allowedCounts = allowed.groupingBy { it }.eachCount() + val offenders = + sites.groupBy { it.key }.filter { (key, found) -> found.size > (allowedCounts[key] ?: 0) }.values.flatten() + + assertTrue( + offenders.isEmpty(), + "Inject ApplicationCoroutineScope or ServiceScope (or take a CoroutineScope parameter) instead of " + + "constructing a scope in the class. New CoroutineScope( sites:\n" + + offenders.joinToString("\n") { it.location }, + ) + } + + @Test + fun `the allowlist still matches real sites`() { + val foundCounts = sites.groupingBy { it.key }.eachCount() + val stale = + allowed.groupingBy { it }.eachCount().filter { (key, count) -> count > (foundCounts[key] ?: 0) }.keys + + assertTrue( + stale.isEmpty(), + "These files construct fewer CoroutineScopes than the allowlist says; take the extra entries off so " + + "this rule keeps verifying something:\n" + + stale.joinToString("\n"), + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ExpectDeclarationTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ExpectDeclarationTest.kt new file mode 100644 index 0000000000..bce79d3e06 --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ExpectDeclarationTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.KoModifier +import com.lemonappdev.konsist.api.Konsist +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Expect classes, objects and interfaces are Beta and need `-Xexpect-actual-classes`, which only the declaring modules' + * build files pass; a platform seam is otherwise an interface bound through Koin or a CompositionLocal, or an `expect + * fun`. The allowlist is the current set, pinned exactly: a new entry needs a reason neither of those serves, and a + * removed one comes off the list. + */ +class ExpectDeclarationTest { + + private val allowed = + setOf( + "org.meshtastic.core.common.util.DateFormatter", + "org.meshtastic.core.database.MeshtasticDatabaseConstructor", + "org.meshtastic.core.takserver.AtakFileWriter", + "org.meshtastic.core.takserver.ZipArchiver", + ) + + private val sourceFiles = Konsist.scopeFromProject().files.filterNot { it.isNestedAgentWorktree() } + + @Test + fun `the scan actually reaches commonMain sources`() { + val paths = sourceFiles.map { it.scanPath }.filter { "/src/commonMain/" in it } + + assertTrue(paths.isNotEmpty(), emptyScanMessage("commonMain scan")) + assertTrue( + paths.any { + it.endsWith("/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/DateFormatter.kt") + }, + "expected core/common commonMain sources in scope; got ${paths.size} files, e.g. ${paths.take(3)}", + ) + } + + @Test + fun `expect classes and objects are exactly the allowlisted set`() { + // KoObjectDeclaration has no hasExpectModifier in this Konsist version, so all three read the modifier list. + val declared = + sourceFiles + .flatMap { file -> + file + .classes() + .filter { KoModifier.EXPECT in it.modifiers } + .map { it.fullyQualifiedName ?: it.name } + + file + .interfaces() + .filter { KoModifier.EXPECT in it.modifiers } + .map { it.fullyQualifiedName ?: it.name } + + file + .objects() + .filter { KoModifier.EXPECT in it.modifiers } + .map { it.fullyQualifiedName ?: it.name } + } + .toSet() + + assertEquals( + allowed, + declared, + "A new expect class or object needs a reason an interface or an expect fun cannot serve; add it to the " + + "allowlist only with one. A removed one comes off the allowlist too.", + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/KotlinTimeSourceTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/KotlinTimeSourceTest.kt new file mode 100644 index 0000000000..ca1a0e2cf8 --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/KotlinTimeSourceTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.Konsist +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * `Instant` and `Clock` come from `kotlin.time`. The kotlinx-datetime compat artifact the build resolves still ships + * the deprecated `kotlinx.datetime` copies, so they keep compiling and only this rule stops them spreading. Reads file + * text, so a fully qualified use counts as well as an import. + */ +class KotlinTimeSourceTest { + + // Assembled so this file does not match its own rule. + private val bannedNames = listOf("Instant", "Clock").map { listOf("kotlinx", "datetime", it).joinToString(".") } + + private val banned = Regex(bannedNames.joinToString("|") { Regex.escape(it) + "\\b" }) + + private val sourceFiles = Konsist.scopeFromProject().files.filterNot { it.isNestedAgentWorktree() } + + @Test + fun `the scan actually reaches project sources`() { + val paths = sourceFiles.map { it.scanPath } + + assertTrue(paths.isNotEmpty(), emptyScanMessage("project-wide scan")) + assertTrue( + paths.any { it.endsWith("/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt") }, + "expected core/model sources in scope; got ${paths.size} files, e.g. ${paths.take(3)}", + ) + } + + @Test + fun `no source uses the kotlinx datetime Instant or Clock`() { + val offenders = sourceFiles.flatMap { file -> + file.text.lines().withIndex().mapNotNull { (index, line) -> + if (banned.containsMatchIn(line)) "${file.scanPath}:${index + 1}: ${line.trim()}" else null + } + } + + assertTrue( + offenders.isEmpty(), + "Use kotlin.time.Instant and kotlin.time.Clock instead of $bannedNames:\n" + offenders.joinToString("\n"), + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/MeasurementSystemSourceTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/MeasurementSystemSourceTest.kt index 9396b35945..6c26629751 100644 --- a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/MeasurementSystemSourceTest.kt +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/MeasurementSystemSourceTest.kt @@ -32,10 +32,10 @@ import kotlin.test.assertTrue class MeasurementSystemSourceTest { /** - * The one legitimate reference: the radio settings screen that *configures* the device's own display units. It - * edits the proto field; it does not format app UI with it. + * The one legitimate reference: the schema label map behind the radio settings picker that *configures* the + * device's own display units. It names the proto enum's values; it does not format app UI with them. */ - private val deviceConfigAllowlist = listOf("feature/settings/", "DisplayConfigItemList.kt") + private val deviceConfigAllowlist = listOf("core/model/", "SchemaEnumLabels.kt") // The rule enforcer itself names the forbidden symbol in its strings, so it is excluded from its own scan. private fun scannedFiles() = Konsist.scopeFromProject() @@ -60,7 +60,7 @@ class MeasurementSystemSourceTest { assertTrue( allowlisted.any { "DisplayConfig.DisplayUnits" in it.text }, - "DisplayConfigItemList.kt no longer references DisplayConfig.DisplayUnits — the allowlist is stale, " + + "SchemaEnumLabels.kt no longer references DisplayConfig.DisplayUnits — the allowlist is stale, " + "update or remove it so this rule keeps verifying something.", ) } diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ModuleBoundaryTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ModuleBoundaryTest.kt new file mode 100644 index 0000000000..339b0e5419 --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/ModuleBoundaryTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.declaration.KoFileDeclaration +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Feature modules sit side by side on top of core: a feature imports no other feature, and core imports no feature. + * Checked on imports rather than build files, so it sees the dependency code uses, not the one a build file declares. + */ +class ModuleBoundaryTest { + + /** The MapLibre renderer draws feature:map's shared map model and map-terrain's elevation tiles. */ + private val allowedFeatureImports = mapOf("map-maplibre" to setOf("map", "map-terrain")) + + private val sourceFiles = Konsist.scopeFromProject().files.filterNot { it.isNestedAgentWorktree() } + + private val featureFiles = sourceFiles.mapNotNull { file -> file.moduleUnder("feature")?.let { file to it } } + + private val coreFiles = sourceFiles.filter { it.moduleUnder("core") != null } + + /** Every package a feature module declares, mapped to the modules that declare it. */ + private val featurePackageOwners: Map> = + featureFiles + .mapNotNull { (file, module) -> file.packagee?.name?.let { it to module } } + .groupBy({ it.first }, { it.second }) + .mapValues { it.value.toSet() } + + private fun KoFileDeclaration.moduleUnder(group: String): String? = + Regex("^/$group/([^/]+)/src/").find(scanPath)?.groupValues?.get(1) + + /** The feature modules that own [import], by the longest declared package it falls under. */ + private fun featureOwnersOf(import: String): Set { + var candidate = import + while ('.' in candidate) { + candidate = candidate.substringBeforeLast('.') + featurePackageOwners[candidate]?.let { + return it + } + } + return emptySet() + } + + private data class FeatureImport(val location: String, val from: String, val owners: Set) + + private fun crossFeatureImports(): List = featureFiles.flatMap { (file, module) -> + file.imports.mapNotNull { import -> + val owners = featureOwnersOf(import.name) + if (owners.isEmpty() || module in owners) { + null + } else { + FeatureImport("${file.scanPath}: ${import.name}", module, owners) + } + } + } + + @Test + fun `the scan actually reaches feature and core sources`() { + assertTrue(featureFiles.isNotEmpty(), emptyScanMessage("feature module scan")) + assertTrue(coreFiles.isNotEmpty(), emptyScanMessage("core module scan")) + assertTrue( + featureFiles.any { (file, _) -> file.scanPath.startsWith("/feature/map-maplibre/src/commonMain/") }, + "expected feature/map-maplibre sources in scope; got ${featureFiles.size} files", + ) + } + + @Test + fun `the feature allowlist still matches real imports`() { + val used = crossFeatureImports().groupBy({ it.from }, { it.owners }).mapValues { it.value.flatten().toSet() } + + allowedFeatureImports.forEach { (from, targets) -> + assertTrue( + used[from].orEmpty().containsAll(targets), + "feature:$from no longer imports all of $targets (it imports ${used[from].orEmpty()}); " + + "shrink the allowlist so this rule keeps verifying something.", + ) + } + } + + @Test + fun `no feature imports another feature outside the allowlist`() { + val offenders = + crossFeatureImports() + .filterNot { import -> import.owners.all { it in allowedFeatureImports[import.from].orEmpty() } } + .map { it.location } + + assertTrue( + offenders.isEmpty(), + "Feature modules share code through core modules, not through each other. Offending imports:\n" + + offenders.joinToString("\n"), + ) + } + + @Test + fun `no core module imports a feature`() { + val offenders = coreFiles.flatMap { file -> + file.imports + .map { it.name } + .filter { it.startsWith("org.meshtastic.feature.") } + .map { "${file.scanPath}: $it" } + } + + assertTrue( + offenders.isEmpty(), + "Core modules sit below every feature and must not import one. Offending imports:\n" + + offenders.joinToString("\n"), + ) + } +} diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/SourceSet.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/SourceSet.kt new file mode 100644 index 0000000000..39b52379cb --- /dev/null +++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/SourceSet.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.konsist + +import com.lemonappdev.konsist.api.declaration.KoFileDeclaration + +/** The source set a file sits in, such as `commonMain` or `jvmTest`, read from its [scanPath]. */ +internal val KoFileDeclaration.sourceSet: String + get() = scanPath.substringAfter("/src/").substringBefore('/') + +/** A file in any test source set (`commonTest`, `jvmTest`, `androidHostTest` and the rest), not production code. */ +internal val KoFileDeclaration.isTestSource: Boolean + get() = sourceSet.contains("test", ignoreCase = true) diff --git a/core/model/detekt-baseline.xml b/core/model/detekt-baseline.xml index 8f17bfa07d..a98a71c43a 100644 --- a/core/model/detekt-baseline.xml +++ b/core/model/detekt-baseline.xml @@ -2,8 +2,16 @@ + AbstractClassCanBeInterface:MeshActivity.kt:MeshActivity$MeshActivity + AbstractClassCanBeInterface:MqttConnectionState.kt:MqttConnectionState$MqttConnectionState + AbstractClassCanBeInterface:MqttProbeStatus.kt:MqttProbeStatus$MqttProbeStatus + NoNameShadowing:ChannelSet.kt:wb PropertyName:DebugUtils.kt:/** * Whether the app is running in debug mode. * * This is a compile-time constant for the shared module. For runtime debug detection, use * [org.meshtastic.core.common.BuildConfigProvider.isDebug] from DI instead. */ @Suppress("ktlint:standard:property-naming", "TopLevelPropertyNaming") const val isDebug: Boolean = false SwallowedException:DataPacket.kt:DataPacket$e: Exception TooGenericExceptionCaught:DataPacket.kt:DataPacket$e: Exception + UseOrEmpty:ChannelSet.kt:host ?: "" + UseOrEmpty:NetworkFirmwareRelease.kt:commit?.takeIf { it.isNotBlank() }?.let { "https://github.com/meshtastic/firmware/commit/$it" } ?: "" + UseOrEmpty:SharedContact.kt:host?.lowercase() ?: "" + UseOrEmpty:UriUtils.kt:uri.host ?: "" diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt index 5e129b863d..73d0ffa85e 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt @@ -17,6 +17,11 @@ package org.meshtastic.core.model import org.meshtastic.core.model.util.isDebug +import org.meshtastic.proto.FieldMetadata +import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.mesh_beacon +import org.meshtastic.proto.statusmessage +import org.meshtastic.proto.tak /** * Defines the capabilities and feature support based on the device firmware version. @@ -31,6 +36,22 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl private fun atLeast(min: DeviceVersion): Boolean = forceEnableAll || (version != null && version >= min) + /** + * Whether a config field is worth offering on this firmware, from the version gates its schema declares. Below + * `since_firmware` the node ignores the field. At or above `deprecated_since` it is shown only while [isSet], so a + * value the node still holds stays visible instead of being silently kept. + */ + fun offers(field: FieldMetadata, isSet: Boolean = false): Boolean { + val arrived = field.since_firmware?.let(::gate)?.let(::atLeast) ?: true + val retired = + field.deprecated_since?.let(::gate)?.let { !forceEnableAll && version != null && version >= it } ?: false + return arrived && (!retired || isSet) + } + + // The schema declares these; an unparseable one must fail here rather than silently pass every gate. + private fun gate(declared: String): DeviceVersion = + DeviceVersion(declared).also { require(it.isValid) { "Unparseable firmware version in schema: $declared" } } + /** Ability to mute notifications from specific nodes via admin messages. */ val canMuteNode = atLeast(V2_7_18) @@ -40,29 +61,24 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl /** Ability to send verified shared contacts. Supported since firmware v2.7.12. */ val canSendVerifiedContacts = atLeast(V2_7_12) - /** Ability to toggle device telemetry globally via module config. Supported since firmware v2.7.12. */ - val canToggleTelemetryEnabled = atLeast(V2_7_12) - /** Ability to toggle the 'is_unmessageable' flag in user config. Supported since firmware v2.6.9. */ val canToggleUnmessageable = atLeast(V2_6_9) /** Support for sharing contact information via QR codes. Supported since firmware v2.6.8. */ val supportsQrCodeSharing = atLeast(V2_6_8) - /** Support for Status Message module. Supported since firmware v2.8.0. */ - val supportsStatusMessage = atLeast(V2_8_0) + /** Support for the Status Message module, from the `since_firmware` its `ModuleConfig` field declares. */ + val supportsStatusMessage = offers(ModuleConfig.statusmessage) /** - * Support for TAK (ATAK) module configuration. Gated to firmware v2.8.0. + * Support for TAK (ATAK) module configuration, from the `since_firmware` its `ModuleConfig` field declares. * - * The v2.7.19 gate this replaces was set on protobuf availability rather than firmware support: v2.7.x - * `AdminModule::handleSetModuleConfig()` has no case for the `tak` submessage, so the node ACKs the write and - * reboots without storing anything, and `NodeDB::saveToDisk()` never sets `has_tak`. The editor therefore appeared - * to save and always read back as unspecified (Meshtastic-Android#6430). - * - * The firmware write, persist and remote-admin read paths land in meshtastic/firmware#11216, labelled for 2.8. + * The schema says 2.8.0 because that is where the firmware gained the write, persist and remote-admin read paths + * (meshtastic/firmware#11216). Before it, v2.7.x `AdminModule::handleSetModuleConfig()` had no case for the `tak` + * submessage: the node ACKed the write and rebooted without storing anything, so the editor appeared to save and + * always read back as unspecified (Meshtastic-Android#6430). */ - val supportsTakConfig = atLeast(V2_8_0) + val supportsTakConfig = offers(ModuleConfig.tak) /** * Support for the v2 TAK port (ATAK_PLUGIN_V2 = 78) with TAKPacketV2 + zstd dictionary compression. Supported since @@ -77,6 +93,13 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl /** Support for ESP32 Unified OTA. Supported since firmware v2.7.18. */ val supportsEsp32Ota = atLeast(V2_7_18) + /** + * Whether a `coding_rate` above the modem preset's own raises it while `use_preset` is on. Supported since firmware + * v2.7.18 (meshtastic/firmware#9155); older firmware uses the preset's rate whatever is stored. `coding_rate` is + * far older than that, so its schema gates cannot answer this. + */ + val supportsCodingRateOverride = atLeast(V2_7_18) + /** * Support for the LoRa region→preset compatibility map. Supported since firmware v2.8.0. Older firmware never sends * the map, so the UI keeps the preset list unconstrained (preset *availability* is [supportsPreset]). @@ -91,11 +114,11 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl val supportsLockdown = atLeast(V2_8_0) /** - * Support for the Mesh Beacon module (`ModuleConfig.MeshBeaconConfig` broadcast/listen). The proto is upstream but - * the firmware module traces to a community fork; gate the config editor to 2.8.0+ so older radios don't show a - * config they'd silently ignore. + * Support for the Mesh Beacon module (`ModuleConfig.MeshBeaconConfig` broadcast/listen), from the `since_firmware` + * its `ModuleConfig` field declares. The proto is upstream but the firmware module traces to a community fork, so + * an older radio would silently ignore the config the editor writes. */ - val supportsMeshBeacon = atLeast(V2_8_0) + val supportsMeshBeacon = offers(ModuleConfig.mesh_beacon) /** * Whether the node reports [NodeInfo.heard_on_current_lora] - whether it has heard each node over RF on the LoRa diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt index a99cdbbb14..1dffbeddc1 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt @@ -108,6 +108,20 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float { } } +/** + * With `use_preset` on, keeps `coding_rate` only while it raises the preset's own coding rate, and stores 0 ("use the + * preset's") otherwise, so a value firmware ignores never sits in the config looking like it applies. A manual config, + * or a preset with no [ChannelOption], comes back unchanged. + */ +fun LoRaConfig.normalizeCodingRateOverride(): LoRaConfig { + val normalized = ChannelOption.from(modem_preset)?.takeIf { use_preset }?.codingRateOverride(coding_rate) + return if (normalized == null || normalized == coding_rate) { + this + } else { + newBuilder().also { wb -> wb.coding_rate = normalized }.build() + } +} + /** * The firmware release that introduced the EU Lite/Narrow and amateur-band ITU regions and the LITE/NARROW/TINY/ * MEDIUM_TURBO presets. NB: the LITE/NARROW *enum values* were vendored into v2.7.23's protobufs, but the radio support @@ -464,6 +478,8 @@ enum class ChannelOption( val modemPreset: ModemPreset, val bandwidth: Float, val spreadingFactor: Int, + /** The coding-rate denominator the preset uses, 5 through 8 for 4/5 through 4/8. */ + val codingRate: Int, val minFirmware: DeviceVersion? = null, ) { // Grouped by range and speed for better readability. @@ -472,26 +488,26 @@ enum class ChannelOption( // older firmware silently falls back to LONG_FAST when sent an unknown preset. // Historical parameters for firmware predating the removal of VERY_LONG_SLOW. - VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, 0.0625f, spreadingFactor = 12), - LONG_TURBO(ModemPreset.LONG_TURBO, 0.500f, spreadingFactor = 11, minFirmware = FIRMWARE_2_7_17), - LONG_FAST(ModemPreset.LONG_FAST, 0.250f, spreadingFactor = 11), - LONG_MODERATE(ModemPreset.LONG_MODERATE, 0.125f, spreadingFactor = 11), - LONG_SLOW(ModemPreset.LONG_SLOW, 0.125f, spreadingFactor = 12), - MEDIUM_FAST(ModemPreset.MEDIUM_FAST, 0.250f, spreadingFactor = 9), - MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, 0.250f, spreadingFactor = 10), - MEDIUM_TURBO(ModemPreset.MEDIUM_TURBO, 0.500f, spreadingFactor = 9, minFirmware = FIRMWARE_2_8), - SHORT_FAST(ModemPreset.SHORT_FAST, 0.250f, spreadingFactor = 7), - SHORT_SLOW(ModemPreset.SHORT_SLOW, 0.250f, spreadingFactor = 8), - SHORT_TURBO(ModemPreset.SHORT_TURBO, 0.500f, spreadingFactor = 7), - LITE_FAST(ModemPreset.LITE_FAST, 0.125f, spreadingFactor = 9, minFirmware = FIRMWARE_2_8), - LITE_SLOW(ModemPreset.LITE_SLOW, 0.125f, spreadingFactor = 10, minFirmware = FIRMWARE_2_8), - NARROW_FAST(ModemPreset.NARROW_FAST, 0.0625f, spreadingFactor = 7, minFirmware = FIRMWARE_2_8), - NARROW_SLOW(ModemPreset.NARROW_SLOW, 0.0625f, spreadingFactor = 8, minFirmware = FIRMWARE_2_8), + VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, 0.0625f, spreadingFactor = 12, codingRate = 8), + LONG_TURBO(ModemPreset.LONG_TURBO, 0.500f, spreadingFactor = 11, codingRate = 8, minFirmware = FIRMWARE_2_7_17), + LONG_FAST(ModemPreset.LONG_FAST, 0.250f, spreadingFactor = 11, codingRate = 5), + LONG_MODERATE(ModemPreset.LONG_MODERATE, 0.125f, spreadingFactor = 11, codingRate = 8), + LONG_SLOW(ModemPreset.LONG_SLOW, 0.125f, spreadingFactor = 12, codingRate = 8), + MEDIUM_FAST(ModemPreset.MEDIUM_FAST, 0.250f, spreadingFactor = 9, codingRate = 5), + MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, 0.250f, spreadingFactor = 10, codingRate = 5), + MEDIUM_TURBO(ModemPreset.MEDIUM_TURBO, 0.500f, spreadingFactor = 9, codingRate = 5, minFirmware = FIRMWARE_2_8), + SHORT_FAST(ModemPreset.SHORT_FAST, 0.250f, spreadingFactor = 7, codingRate = 5), + SHORT_SLOW(ModemPreset.SHORT_SLOW, 0.250f, spreadingFactor = 8, codingRate = 5), + SHORT_TURBO(ModemPreset.SHORT_TURBO, 0.500f, spreadingFactor = 7, codingRate = 5), + LITE_FAST(ModemPreset.LITE_FAST, 0.125f, spreadingFactor = 9, codingRate = 5, minFirmware = FIRMWARE_2_8), + LITE_SLOW(ModemPreset.LITE_SLOW, 0.125f, spreadingFactor = 10, codingRate = 5, minFirmware = FIRMWARE_2_8), + NARROW_FAST(ModemPreset.NARROW_FAST, 0.0625f, spreadingFactor = 7, codingRate = 6, minFirmware = FIRMWARE_2_8), + NARROW_SLOW(ModemPreset.NARROW_SLOW, 0.0625f, spreadingFactor = 8, codingRate = 6, minFirmware = FIRMWARE_2_8), // 15.625 kHz LoRa bandwidth (firmware modemPresetToParams; the proto's "20kHz" is the // padded channel spacing, not the modem bandwidth used for numChannels/radioFreq math). - TINY_FAST(ModemPreset.TINY_FAST, 0.015625f, spreadingFactor = 7, minFirmware = FIRMWARE_2_8), - TINY_SLOW(ModemPreset.TINY_SLOW, 0.015625f, spreadingFactor = 8, minFirmware = FIRMWARE_2_8), + TINY_FAST(ModemPreset.TINY_FAST, 0.015625f, spreadingFactor = 7, codingRate = 5, minFirmware = FIRMWARE_2_8), + TINY_SLOW(ModemPreset.TINY_SLOW, 0.015625f, spreadingFactor = 8, codingRate = 6, minFirmware = FIRMWARE_2_8), ; // Semtech demodulation floor: -7.5 dB at SF7, improving 2.5 dB per SF step. @@ -499,7 +515,24 @@ enum class ChannelOption( val snrLimit: Float get() = SNR_FLOOR_SF7_DB - SNR_FLOOR_PER_SF_DB * (spreadingFactor - MIN_SPREADING_FACTOR) + /** + * The coding rates `coding_rate` can raise this preset to while `use_preset` is on. Firmware applies it over the + * preset only when it is higher than [codingRate] ([Capabilities.supportsCodingRateOverride]), so nothing at or + * below that is offered, and a preset already at 4/8 has none. + */ + val codingRateOverrides: IntRange + get() = (codingRate + 1)..MAX_CODING_RATE + + /** [stored] as firmware applies it over this preset: the override while it raises [codingRate], else 0. */ + fun codingRateOverride(stored: Int): Int = if (stored in codingRateOverrides) stored else 0 + + /** The coding rate the radio uses on this preset with [stored] in `coding_rate`. */ + fun effectiveCodingRate(stored: Int): Int = codingRateOverride(stored).takeIf { it != 0 } ?: codingRate + companion object { + /** The most redundant LoRa coding rate, 4/8. */ + const val MAX_CODING_RATE = 8 + /** SF7's demodulation floor, the anchor for [snrLimit]. */ private const val SNR_FLOOR_SF7_DB = -7.5f diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt index b290c47db4..ea8fa0ebc1 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt @@ -65,6 +65,11 @@ data class DataPacket( var transportMechanism: Int = 0, /** True when the radio verified this broadcast's XEdDSA signature ([MeshPacket.xeddsa_signed]). */ var xeddsaSigned: Boolean = false, + /** + * The radio's verdict on the ack that closed out this outgoing packet (see [MeshPacket.AckProofStatus]). Written + * when the ack arrives, not when the packet is sent, so it stays absent for anything still enroute. + */ + var ackProofStatus: Int = 0, ) { /** If there was an error with this message, this string describes what was wrong. */ diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceAddress.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceAddress.kt new file mode 100644 index 0000000000..be7e19c466 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceAddress.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.meshtastic.core.common.util.isValidDeviceAddress + +/** + * A selected-radio address, parsed from its stored form: one [InterfaceId] character followed by the transport's own + * identity for the device. + * + * [raw] is the persisted string and stays the key for the per-device database and every per-device preference, so it is + * never rewritten. The legacy `!` BLE prefix therefore keeps its `!` in [raw] and only reads as + * [InterfaceId.BLUETOOTH]. + */ +class DeviceAddress private constructor(val raw: String, val interfaceId: InterfaceId) { + + /** The address without its transport prefix: a BLE MAC, a `host:port`, a USB serial key. */ + val identity: String + get() = raw.substring(1) + + override fun equals(other: Any?): Boolean = other is DeviceAddress && other.raw == raw + + override fun hashCode(): Int = raw.hashCode() + + override fun toString(): String = "DeviceAddress($interfaceId)" + + companion object { + private const val LEGACY_BLUETOOTH_PREFIX = '!' + + /** + * Parses [raw], or returns `null` for no selection: `null`, blank, any no-device sentinel, or an unknown + * prefix. + */ + fun parse(raw: String?): DeviceAddress? { + if (raw == null || !isValidDeviceAddress(raw)) return null + val prefix = raw.first() + val interfaceId = + if (prefix == LEGACY_BLUETOOTH_PREFIX) InterfaceId.BLUETOOTH else InterfaceId.forIdChar(prefix) + return interfaceId?.takeUnless { it == InterfaceId.NOP }?.let { DeviceAddress(raw, it) } + } + } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceHardware.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceHardware.kt index b17fb6012f..16965496c9 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceHardware.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceHardware.kt @@ -28,6 +28,11 @@ data class DeviceHardware( val hwModel: Int = 0, val hwModelSlug: String = "", val images: List? = null, + /** + * The registry's maker relationship: independent hardware, as opposed to the project's own lifecycle flag + * [activelySupported]. Absent and `false` mean the same thing; how the two combine is [supportTier]. + */ + val isMaker: Boolean = false, val partitionScheme: String? = null, val platformioTarget: String = "", val requiresDfu: Boolean? = null, diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceMetrics.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceMetrics.kt deleted file mode 100644 index 963712db7c..0000000000 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceMetrics.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.model - -import org.meshtastic.core.common.util.nowSeconds - -data class DeviceMetrics( - val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) - val batteryLevel: Int = 0, - val voltage: Float, - val channelUtilization: Float, - val airUtilTx: Float, - val uptimeSeconds: Int, -) { - companion object { - @Suppress("MagicNumber") - fun currentTime() = nowSeconds.toInt() - } - - /** Create our model object from a protobuf. */ - constructor( - p: org.meshtastic.proto.DeviceMetrics, - telemetryTime: Int = currentTime(), - ) : this( - telemetryTime, - p.battery_level ?: 0, - p.voltage ?: 0f, - p.channel_utilization ?: 0f, - p.air_util_tx ?: 0f, - p.uptime_seconds ?: 0, - ) -} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceType.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceType.kt index 37b04026a5..26e2c27eb1 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceType.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceType.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.core.model -/** Represent the different ways a device can connect to the client. */ +/** The physical transports a radio can be reached over. Demo Mode reaches no radio and has no [DeviceType]. */ enum class DeviceType { BLE, TCP, @@ -28,13 +28,12 @@ enum class DeviceType { when (InterfaceId.forIdChar(address.firstOrNull() ?: return null)) { InterfaceId.BLUETOOTH -> BLE - InterfaceId.SERIAL, - InterfaceId.MOCK, // Mock/demo mode historically presents as USB. - -> USB + InterfaceId.SERIAL -> USB InterfaceId.TCP -> TCP InterfaceId.NOP, + InterfaceId.MOCK, InterfaceId.REPLAY, null, -> null diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/BuildUtils.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ExcludedModule.kt similarity index 66% rename from core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/BuildUtils.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/ExcludedModule.kt index 127e05b3da..afde12e6da 100644 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/BuildUtils.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ExcludedModule.kt @@ -14,13 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.common.util +package org.meshtastic.core.model -/** Utility for checking build properties, such as emulator detection. */ -expect object BuildUtils { - /** Whether the app is currently running on an emulator. */ - val isEmulator: Boolean +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.ExcludedModules - /** The SDK version of the current platform. On non-Android platforms, this returns 0. */ - val sdkInt: Int -} +/** Whether the node reports [module] compiled out of its firmware. Metadata not read yet excludes nothing. */ +fun DeviceMetadata?.excludes(module: ExcludedModules): Boolean = ((this?.excluded_modules ?: 0) and module.value) != 0 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/FirmwareRelease.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/FirmwareRelease.kt new file mode 100644 index 0000000000..7c923e2d4c --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/FirmwareRelease.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.meshtastic.core.common.util.nowMillis + +data class FirmwareRelease( + val id: String = "", + val pageUrl: String = "", + val releaseNotes: String = "", + val title: String = "", + val zipUrl: String = "", + val lastUpdated: Long = nowMillis, + val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE, +) + +fun FirmwareRelease.asDeviceVersion(): DeviceVersion = DeviceVersion(id.substringBeforeLast(".").replace("v", "")) + +enum class FirmwareReleaseType { + STABLE, + ALPHA, + + /** Nightly preview from the nightly host's root; gated behind the modules unlock. */ + NIGHTLY, + LOCAL, +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/HardwareSupportTier.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/HardwareSupportTier.kt new file mode 100644 index 0000000000..e27852e7b8 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/HardwareSupportTier.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +/** + * The rung a board reads at wherever the app names its support status. Top to bottom: hardware the project actively + * supports, independent maker hardware, then community hardware (including anything not actively supported). + */ +enum class HardwareSupportTier { + SUPPORTED, + MAKER, + COMMUNITY, +} + +/** The registry's `supportLevel` for legacy hardware; 1 is flagship and 2 is niche. */ +private const val SUPPORT_LEVEL_LEGACY = 3 + +/** + * Resolves the rung in one place so every surface agrees. [DeviceHardware.isMaker] is a relationship and wins first, so + * a maker board never reads as plain supported once the registry promotes it; [DeviceHardware.activelySupported] is the + * project's lifecycle flag and decides the other two rungs. A legacy or untiered maker board falls through to that + * lifecycle check. + */ +val DeviceHardware.supportTier: HardwareSupportTier + get() = + when { + isMaker && supportLevel != null && supportLevel < SUPPORT_LEVEL_LEGACY -> HardwareSupportTier.MAKER + activelySupported -> HardwareSupportTier.SUPPORTED + else -> HardwareSupportTier.COMMUNITY + } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt index 9b592e3f9d..fbdf06a6cd 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt @@ -26,6 +26,10 @@ enum class InterfaceId(val id: Char) { TCP('t'), ; + /** True for the Demo Mode transports, which answer locally and reach no radio. */ + val isVirtual: Boolean + get() = this == MOCK || this == REPLAY + companion object { fun forIdChar(id: Char): InterfaceId? = entries.firstOrNull { it.id == id } } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt index e28e22a612..02c1291b42 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt @@ -51,9 +51,11 @@ import org.meshtastic.core.resources.message_routing_error_rate_limit_exceeded_d import org.meshtastic.core.resources.message_routing_error_timeout_detail import org.meshtastic.core.resources.message_routing_error_too_large import org.meshtastic.core.resources.message_routing_error_too_large_detail +import org.meshtastic.core.resources.message_status_ack_proof_invalid import org.meshtastic.core.resources.message_status_delivered import org.meshtastic.core.resources.message_status_enroute import org.meshtastic.core.resources.message_status_recipient_delivered +import org.meshtastic.core.resources.message_status_recipient_delivered_proven import org.meshtastic.core.resources.message_status_relayed_not_confirmed import org.meshtastic.core.resources.message_status_sfpp_confirmed import org.meshtastic.core.resources.message_status_sfpp_routing @@ -76,6 +78,12 @@ import org.meshtastic.core.resources.routing_error_pki_unknown_pubkey import org.meshtastic.core.resources.routing_error_rate_limit_exceeded import org.meshtastic.core.resources.routing_error_timeout import org.meshtastic.core.resources.routing_error_too_large +import org.meshtastic.core.resources.security_ack_proof_failed +import org.meshtastic.core.resources.security_ack_proof_failed_info +import org.meshtastic.core.resources.security_ack_proof_no_key +import org.meshtastic.core.resources.security_ack_proof_no_key_info +import org.meshtastic.core.resources.security_ack_proof_verified +import org.meshtastic.core.resources.security_ack_proof_verified_info import org.meshtastic.core.resources.unrecognized import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Routing @@ -183,40 +191,83 @@ fun getMessageRoutingErrorDetailResFrom(routingError: Int): StringResource? = wh fun getMessageStatusDetailRes(status: MessageStatus?, routingError: Int): StringResource? = if (status == MessageStatus.ERROR) getMessageRoutingErrorDetailResFrom(routingError) else null +/** + * The ack proof rows the message detail sheet shows, or null when no proof was carried - the ordinary case, and every + * ack from firmware that predates [MeshPacket.AckProofStatus]. + */ +fun getAckProofStatusRes(ackProofStatus: Int): Pair? = + when (MeshPacket.AckProofStatus.fromValue(ackProofStatus)) { + MeshPacket.AckProofStatus.ACK_PROOF_VALID -> + Res.string.security_ack_proof_verified to Res.string.security_ack_proof_verified_info + + MeshPacket.AckProofStatus.ACK_PROOF_INVALID -> + Res.string.security_ack_proof_failed to Res.string.security_ack_proof_failed_info + + MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY -> + Res.string.security_ack_proof_no_key to Res.string.security_ack_proof_no_key_info + + MeshPacket.AckProofStatus.ACK_PROOF_ABSENT, + null, + -> null + } + +/** + * True when an ack carried a proof and it did not verify, which is an attempted forgery rather than a quiet absence. + */ +fun isAckProofForged(ackProofStatus: Int): Boolean = + MeshPacket.AckProofStatus.fromValue(ackProofStatus) == MeshPacket.AckProofStatus.ACK_PROOF_INVALID + fun getMessageStatusStringRes( status: MessageStatus?, routingError: Int, isDirectMessage: Boolean = false, + ackProofStatus: Int = 0, ): Pair { val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status + val proven = MeshPacket.AckProofStatus.fromValue(ackProofStatus) == MeshPacket.AckProofStatus.ACK_PROOF_VALID val text = - when (status) { - MessageStatus.RECEIVED -> Res.string.message_status_recipient_delivered - - MessageStatus.QUEUED -> Res.string.message_status_enroute - - MessageStatus.ENROUTE -> Res.string.message_status_enroute - - MessageStatus.SFPP_ROUTING -> Res.string.message_status_sfpp_routing - - MessageStatus.SFPP_CONFIRMED -> Res.string.message_status_sfpp_confirmed - - MessageStatus.DELIVERED -> - if (isDirectMessage) { - Res.string.message_status_relayed_not_confirmed - } else { - Res.string.message_status_delivered - } - - MessageStatus.ERROR -> getMessageRoutingErrorStringResFrom(routingError) - - MessageStatus.UNKNOWN, - null, - -> Res.string.message_status_unknown + when { + isAckProofForged(ackProofStatus) -> Res.string.message_status_ack_proof_invalid + else -> messageStatusText(status, routingError, isDirectMessage, proven) } return title to text } +private fun messageStatusText( + status: MessageStatus?, + routingError: Int, + isDirectMessage: Boolean, + proven: Boolean, +): StringResource = when (status) { + MessageStatus.RECEIVED -> + if (proven) { + Res.string.message_status_recipient_delivered_proven + } else { + Res.string.message_status_recipient_delivered + } + + MessageStatus.QUEUED -> Res.string.message_status_enroute + + MessageStatus.ENROUTE -> Res.string.message_status_enroute + + MessageStatus.SFPP_ROUTING -> Res.string.message_status_sfpp_routing + + MessageStatus.SFPP_CONFIRMED -> Res.string.message_status_sfpp_confirmed + + MessageStatus.DELIVERED -> + if (isDirectMessage) { + Res.string.message_status_relayed_not_confirmed + } else { + Res.string.message_status_delivered + } + + MessageStatus.ERROR -> getMessageRoutingErrorStringResFrom(routingError) + + MessageStatus.UNKNOWN, + null, + -> Res.string.message_status_unknown +} + fun isMessageStatusRetryable(status: MessageStatus?, routingError: Int, isDirectMessage: Boolean = false): Boolean = when { status == MessageStatus.DELIVERED && isDirectMessage -> true @@ -260,6 +311,8 @@ data class Message( val transportMechanism: Int = 0, /** True when the radio verified this broadcast's XEdDSA signature ([MeshPacket.xeddsa_signed]). */ val xeddsaSigned: Boolean = false, + /** The radio's verdict on the ack that delivered this message (see [MeshPacket.AckProofStatus]). */ + val ackProofStatus: Int = 0, /** On-device translation of [text], persisted so the user can toggle back to it without re-translating. */ val translatedText: String? = null, /** Whether the bubble currently displays [translatedText] instead of [text]. */ @@ -281,7 +334,7 @@ data class Message( if (showTranslated && translatedText != null && !searching) translatedText else text fun getStatusStringRes(isDirectMessage: Boolean = false): Pair = - getMessageStatusStringRes(status, routingError, isDirectMessage) + getMessageStatusStringRes(status, routingError, isDirectMessage, ackProofStatus) fun getStatusDetailRes(): StringResource? = getMessageStatusDetailRes(status, routingError) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt index cd25165e1e..a9c66c30c6 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt @@ -33,6 +33,14 @@ sealed class MqttConnectionState { /** The MQTT client is connected and subscribed to topics. */ data object Connected : MqttConnectionState() + /** + * The MQTT client is connected, but the broker refused some or all of the topic filters it asked for. + * + * @property refused Refused topic filter to the broker's reason code name (for example `NOT_AUTHORIZED`). + * @property granted How many filters the broker accepted; `0` means the proxy hears nothing from the broker. + */ + data class SubscriptionRefused(val refused: Map, val granted: Int) : MqttConnectionState() + /** * The MQTT client lost connection and is attempting to reconnect. * diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceHardware.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceHardware.kt index ff0d7db914..6563f60d59 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceHardware.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceHardware.kt @@ -33,6 +33,7 @@ data class NetworkDeviceHardware( @SerialName("hwModel") val hwModel: Int = 0, @SerialName("hwModelSlug") val hwModelSlug: String = "", @SerialName("images") val images: List? = null, + @SerialName("isMaker") val isMaker: Boolean = false, @SerialName("key") val key: String? = null, @SerialName("partitionScheme") val partitionScheme: String? = null, @SerialName("platformioTarget") val platformioTarget: String = "", diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index a16a0bb693..866bd9a536 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -210,7 +210,7 @@ data class Node( val soilMoisture = soil_moisture?.takeIf { it in soilMoistureRange }?.let { MetricFormatter.percent(it) } val voltage = this.voltage?.let { MetricFormatter.voltage(it) } val current = current?.let { MetricFormatter.current(it) } - val iaq = if ((iaq ?: 0) != 0) "IAQ: $iaq" else null + val iaq = iaq?.let { "IAQ: $it" } return listOfNotNull( paxcounter.getDisplayString(), @@ -302,7 +302,7 @@ private val Node.unsignedNum: Long get() = num.toLong().let { if (it < 0) it + UNSIGNED_INT_OFFSET else it } /** - * Matches node search text (long/short name, hex id, decimal id) with Unicode-aware case folding. + * Matches node search text (long/short name, hex id, decimal id, status message) with Unicode-aware case folding. * * Must run in Kotlin, not SQL: SQLite's LIKE/UPPER/LOWER only case-fold ASCII a-z/A-Z, so a query like "kolså" can * never match a stored name of "KOLSÅS" via a SQL WHERE clause (#6750). @@ -311,4 +311,5 @@ fun Node.matchesSearch(filter: String): Boolean = filter.isBlank() || user.long_name.contains(filter, ignoreCase = true) || user.short_name.contains(filter, ignoreCase = true) || user.id.contains(filter, ignoreCase = true) || + nodeStatus?.contains(filter, ignoreCase = true) == true || unsignedNum.toString().contains(filter, ignoreCase = true) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Position.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Position.kt index 58c97304e5..5447858afe 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Position.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Position.kt @@ -24,7 +24,8 @@ import org.meshtastic.core.model.util.anonymize data class Position( val latitude: Double, val longitude: Double, - val altitude: Int, + /** Metres above mean sea level, or null when the fix reported none. 0 is sea level, not absence. */ + val altitude: Int?, val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) val satellitesInView: Int = 0, val groundSpeed: Int = 0, @@ -51,7 +52,7 @@ data class Position( ) : this( degD(position.latitude_i ?: 0), degD(position.longitude_i ?: 0), - position.altitude ?: 0, + position.altitude, if (position.time != 0) position.time else defaultTime, position.sats_in_view, position.ground_speed ?: 0, @@ -66,7 +67,7 @@ data class Position( fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude) /** Returns whether this position represents the protocol sentinel for removing a fixed position. */ - fun isFixedPositionRemoval(): Boolean = latitude == 0.0 && longitude == 0.0 && altitude == 0 + fun isFixedPositionRemoval(): Boolean = latitude == 0.0 && longitude == 0.0 && (altitude == null || altitude == 0) @Suppress("MagicNumber") fun isValid(): Boolean = latitude != 0.0 && @@ -77,3 +78,16 @@ data class Position( override fun toString(): String = "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)" } + +/** + * The raw `latitude_i` and `longitude_i`, or null when either is absent or both are exactly 0. A 0 stand-in for a + * missing axis, or a 0,0 report, puts the node on the equator or the prime meridian where it never was. + */ +fun org.meshtastic.proto.Position.fixOrNull(): Pair? { + val latI = latitude_i + val lonI = longitude_i + return if (latI == null || lonI == null || (latI == 0 && lonI == 0)) null else latI to lonI +} + +/** Whether this report places the node on a map; see [fixOrNull]. */ +fun org.meshtastic.proto.Position.hasFix(): Boolean = fixOrNull() != null diff --git a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/QuickChatAction.kt similarity index 72% rename from feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/QuickChatAction.kt index fd13a3b95d..cf80bd7f3d 100644 --- a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/QuickChatAction.kt @@ -14,12 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.settings.tak +package org.meshtastic.core.model -import androidx.compose.runtime.Composable - -@Composable -actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit = - { _ -> - // No-op on iOS for now +data class QuickChatAction( + val uuid: Long = 0L, + val name: String = "", + val message: String = "", + val mode: Mode = Mode.Instant, + val position: Int, +) { + enum class Mode { + Append, + Instant, } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt index 8233333429..b327574604 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.model import okio.ByteString +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.User data class Reaction( @@ -39,4 +40,8 @@ data class Reaction( val to: String? = null, val channel: Int = 0, val sfppHash: ByteString? = null, + /** True when the radio verified this broadcast reaction's XEdDSA signature ([MeshPacket.xeddsa_signed]). */ + val xeddsaSigned: Boolean = false, + /** The radio's verdict on the ack that delivered this outgoing reaction (see [MeshPacket.AckProofStatus]). */ + val ackProofStatus: Int = 0, ) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/SchemaEnumLabels.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/SchemaEnumLabels.kt new file mode 100644 index 0000000000..20f672659c --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/SchemaEnumLabels.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.allStringResources +import org.meshtastic.proto.Config +import org.meshtastic.proto.MemberRole +import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.Team + +// Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs 2.8.0.116-g51028ca-SNAPSHOT. +// Do not edit: change the schema, then run sync. + +/** + * The schema's label for this enum value, or null where the schema does not name it. A picker shows this in place of + * the constant's Kotlin name, and falls back to that name when it is null, so a value the schema has not reached still + * renders. + */ +@Suppress("CyclomaticComplexMethod") +fun Enum<*>.schemaLabelRes(): StringResource? = when (this) { + is Config.BluetoothConfig.PairingMode -> schemaLabel("schema_bluetooth_pairingmode_") + is Config.DeviceConfig.RebroadcastMode -> schemaLabel("schema_device_rebroadcastmode_") + is Config.DeviceConfig.Role -> schemaLabel("schema_device_role_") + is Config.DisplayConfig.CompassOrientation -> schemaLabel("schema_display_compassorientation_") + is Config.DisplayConfig.DisplayMode -> schemaLabel("schema_display_displaymode_") + is Config.DisplayConfig.DisplayUnits -> schemaLabel("schema_display_displayunits_") + is Config.DisplayConfig.OledType -> schemaLabel("schema_display_oledtype_") + is Config.LoRaConfig.ModemPreset -> schemaLabel("schema_lora_modempreset_") + is Config.LoRaConfig.RegionCode -> schemaLabel("schema_lora_regioncode_") + is Config.NetworkConfig.AddressMode -> schemaLabel("schema_network_addressmode_") + is Config.NetworkConfig.ProtocolFlags -> schemaLabel("schema_network_protocolflags_") + is Config.PositionConfig.GpsMode -> schemaLabel("schema_position_gpsmode_") + is Config.PositionConfig.PositionFlags -> schemaLabel("schema_position_positionflags_") + is Config.SecurityConfig.PacketSignaturePolicy -> schemaLabel("schema_security_packetsignaturepolicy_") + is MemberRole -> schemaLabel("schema_memberrole_") + is ModuleConfig.AudioConfig.Audio_Baud -> schemaLabel("schema_audio_audio_baud_") + is ModuleConfig.CannedMessageConfig.InputEventChar -> schemaLabel("schema_cannedmessage_inputeventchar_") + is ModuleConfig.DetectionSensorConfig.TriggerType -> schemaLabel("schema_detectionsensor_triggertype_") + is ModuleConfig.SerialConfig.Serial_Baud -> schemaLabel("schema_serial_serial_baud_") + is ModuleConfig.SerialConfig.Serial_Mode -> schemaLabel("schema_serial_serial_mode_") + is Team -> schemaLabel("schema_team_") + else -> null +} + +/** The schema's one-sentence explanation of this enum value, or null where it has none. */ +@Suppress("CyclomaticComplexMethod") +fun Enum<*>.schemaDescriptionRes(): StringResource? = when (this) { + is Config.DeviceConfig.RebroadcastMode -> schemaDescription("schema_device_rebroadcastmode_") + is Config.DeviceConfig.Role -> schemaDescription("schema_device_role_") + is Config.PositionConfig.PositionFlags -> schemaDescription("schema_position_positionflags_") + is Config.SecurityConfig.PacketSignaturePolicy -> schemaDescription("schema_security_packetsignaturepolicy_") + else -> null +} + +/** + * The resource prefix every value of a labelled enum shares, one per enum. A consumer that has to tell an enum value's + * resource from a field's - settings search, which indexes fields and not the values they offer - reads this rather + * than keeping its own copy of the list. + */ +val schemaEnumValuePrefixes: Set = + setOf( + "schema_bluetooth_pairingmode_", + "schema_device_rebroadcastmode_", + "schema_device_role_", + "schema_display_compassorientation_", + "schema_display_displaymode_", + "schema_display_displayunits_", + "schema_display_oledtype_", + "schema_lora_modempreset_", + "schema_lora_regioncode_", + "schema_network_addressmode_", + "schema_network_protocolflags_", + "schema_position_gpsmode_", + "schema_position_positionflags_", + "schema_security_packetsignaturepolicy_", + "schema_memberrole_", + "schema_audio_audio_baud_", + "schema_cannedmessage_inputeventchar_", + "schema_detectionsensor_triggertype_", + "schema_serial_serial_baud_", + "schema_serial_serial_mode_", + "schema_team_", + ) + +private fun Enum<*>.schemaLabel(prefix: String): StringResource? = Res.allStringResources[prefix + name.lowercase()] + +private fun Enum<*>.schemaDescription(prefix: String): StringResource? = + Res.allStringResources[prefix + name.lowercase() + "_description"] diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt index 7ae5293f8d..20b34058f4 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt @@ -16,66 +16,8 @@ */ package org.meshtastic.core.model -import org.jetbrains.compose.resources.StringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.tak_role_forwardobserver -import org.meshtastic.core.resources.tak_role_hq -import org.meshtastic.core.resources.tak_role_k9 -import org.meshtastic.core.resources.tak_role_medic -import org.meshtastic.core.resources.tak_role_rto -import org.meshtastic.core.resources.tak_role_sniper -import org.meshtastic.core.resources.tak_role_teamlead -import org.meshtastic.core.resources.tak_role_teammember -import org.meshtastic.core.resources.tak_role_unspecified -import org.meshtastic.core.resources.tak_team_blue -import org.meshtastic.core.resources.tak_team_brown -import org.meshtastic.core.resources.tak_team_cyan -import org.meshtastic.core.resources.tak_team_dark_blue -import org.meshtastic.core.resources.tak_team_dark_green -import org.meshtastic.core.resources.tak_team_green -import org.meshtastic.core.resources.tak_team_magenta -import org.meshtastic.core.resources.tak_team_maroon -import org.meshtastic.core.resources.tak_team_orange -import org.meshtastic.core.resources.tak_team_purple -import org.meshtastic.core.resources.tak_team_red -import org.meshtastic.core.resources.tak_team_teal -import org.meshtastic.core.resources.tak_team_unspecified_color -import org.meshtastic.core.resources.tak_team_white -import org.meshtastic.core.resources.tak_team_yellow -import org.meshtastic.proto.MemberRole import org.meshtastic.proto.Team -@Suppress("CyclomaticComplexMethod") -fun getStringResFrom(team: Team): StringResource = when (team) { - Team.Unspecifed_Color -> Res.string.tak_team_unspecified_color - Team.White -> Res.string.tak_team_white - Team.Yellow -> Res.string.tak_team_yellow - Team.Orange -> Res.string.tak_team_orange - Team.Magenta -> Res.string.tak_team_magenta - Team.Red -> Res.string.tak_team_red - Team.Maroon -> Res.string.tak_team_maroon - Team.Purple -> Res.string.tak_team_purple - Team.Dark_Blue -> Res.string.tak_team_dark_blue - Team.Blue -> Res.string.tak_team_blue - Team.Cyan -> Res.string.tak_team_cyan - Team.Teal -> Res.string.tak_team_teal - Team.Green -> Res.string.tak_team_green - Team.Dark_Green -> Res.string.tak_team_dark_green - Team.Brown -> Res.string.tak_team_brown -} - -fun getStringResFrom(role: MemberRole): StringResource = when (role) { - MemberRole.Unspecifed -> Res.string.tak_role_unspecified - MemberRole.TeamMember -> Res.string.tak_role_teammember - MemberRole.TeamLead -> Res.string.tak_role_teamlead - MemberRole.HQ -> Res.string.tak_role_hq - MemberRole.Sniper -> Res.string.tak_role_sniper - MemberRole.Medic -> Res.string.tak_role_medic - MemberRole.ForwardObserver -> Res.string.tak_role_forwardobserver - MemberRole.RTO -> Res.string.tak_role_rto - MemberRole.K9 -> Res.string.tak_role_k9 -} - @Suppress("CyclomaticComplexMethod", "MagicNumber") fun getColorFrom(team: Team): Long = when (team) { Team.Unspecifed_Color -> 0xFF00FFFF diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/geofence/ActiveWaypoints.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/geofence/ActiveWaypoints.kt index 4ad7d4ee58..0ba73f07b2 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/geofence/ActiveWaypoints.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/geofence/ActiveWaypoints.kt @@ -27,10 +27,12 @@ import org.meshtastic.proto.Waypoint * `packet` row (keyed on the random MeshPacket transmission id, not the semantic waypoint id). Consumers must normalise * — latest transmission wins, expired waypoints dropped — or they will see duplicate/stale geofences and keep alerting * on waypoints the user can no longer see. Both the map UI and the geofence engine go through here so they cannot - * drift. Rows are ordered oldest-first, so `associateBy` keeps the newest copy per id. + * drift. Rows are ordered oldest-first, so `toMap` keeps the newest copy per id. */ -fun List.activeWaypointPackets(nowSeconds: Long): Map = filter { it.waypoint != null } - .associateBy { it.waypoint!!.id } +fun List.activeWaypointPackets(nowSeconds: Long): Map = mapNotNull { packet -> + packet.waypoint?.let { it.id to packet } +} + .toMap() .filterValues { val expire = it.waypoint?.expire ?: 0 expire == 0 || expire.toLong() > nowSeconds @@ -42,7 +44,8 @@ fun List.activeWaypointPackets(nowSeconds: Long): Map.geofencesToMonitor(myNodeNum: Int?, optedInIds: Set): List = - filter { it.isFromLocal(myNodeNum) || (it.waypoint?.id in optedInIds) } - .mapNotNull { it.waypoint } - .filter { it.notifiesOnCrossing } +fun Collection.geofencesToMonitor(myNodeNum: Int?, optedInIds: Set): List = filter { + it.isFromLocal(myNodeNum) || (it.waypoint?.id in optedInIds) +} + .mapNotNull { it.waypoint } + .filter { it.notifiesOnCrossing } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/AirQualityIndex.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/AirQualityIndex.kt index e8d5868143..1ef805e14b 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/AirQualityIndex.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/AirQualityIndex.kt @@ -30,7 +30,6 @@ import kotlin.math.pow object AirQualityIndex { private const val NOWCAST_WINDOW_HOURS = 12 - private const val SECONDS_PER_HOUR = 3600L /** EPA requires the most recent hour plus at least 2 of the 3 most recent hours, or NowCast isn't reported. */ private const val MIN_VALID_HOURS = 2 @@ -49,7 +48,7 @@ object AirQualityIndex { val sums = DoubleArray(NOWCAST_WINDOW_HOURS) val counts = IntArray(NOWCAST_WINDOW_HOURS) for ((time, pm25) in readings) { - val hoursAgo = (nowEpochSeconds - time) / SECONDS_PER_HOUR + val hoursAgo = (nowEpochSeconds - time) / TimeConstants.SECONDS_PER_HOUR if (hoursAgo in 0 until NOWCAST_WINDOW_HOURS) { sums[hoursAgo.toInt()] += pm25 counts[hoursAgo.toInt()]++ @@ -87,7 +86,7 @@ object AirQualityIndex { * of readings per 12-hour window rather than quadratic over the whole time frame. */ fun nowCastAqiSeries(readings: List>): List { - val windowSeconds = NOWCAST_WINDOW_HOURS * SECONDS_PER_HOUR + val windowSeconds = NOWCAST_WINDOW_HOURS.toLong() * TimeConstants.SECONDS_PER_HOUR var start = 0 return readings.mapIndexed { index, (time, _) -> // Readings at or before this cutoff fall outside the point's own 12h window, so drop them from the front. diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt index 252e59f9f7..718eee88b5 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt @@ -44,17 +44,17 @@ fun CommonUri.toChannelSet(): ChannelSet { h.equals(MESHTASTIC_HOST, ignoreCase = true) || h.equals("www.$MESHTASTIC_HOST", ignoreCase = true) val segments = pathSegments val isCorrectPath = segments.any { it.equals("e", ignoreCase = true) } - val hasFragment = !fragment.isNullOrBlank() + val frag = fragment - if (!hasFragment || !isCorrectHost || !isCorrectPath) { + if (frag.isNullOrBlank() || !isCorrectHost || !isCorrectPath) { throw MalformedMeshtasticUrlException( - "Not a valid Meshtastic URL: host=$h, segmentCount=${segments.size}, hasFragment=$hasFragment", + "Not a valid Meshtastic URL: host=$h, segmentCount=${segments.size}, hasFragment=${!frag.isNullOrBlank()}", ) } // Older versions of Meshtastic clients (Apple/web) included `?add=true` within the URL fragment. // This gracefully handles those cases until the newer version are generally available/used. - val fragmentBase64 = fragment!!.substringBefore('?').replace('-', '+').replace('_', '/') + val fragmentBase64 = frag.substringBefore('?').replace('-', '+').replace('_', '/') val fragmentBytes = fragmentBase64.decodeBase64() ?: throw MalformedMeshtasticUrlException("Invalid Base64 in URL fragment") val url = @@ -63,7 +63,7 @@ fun CommonUri.toChannelSet(): ChannelSet { } catch (e: Exception) { throw MalformedMeshtasticUrlException("Failed to decode channel set: ${e::class.simpleName}", e) } - val shouldAdd = fragment?.substringAfter('?', "")?.addParameter() ?: getBooleanQueryParameter("add", false) + val shouldAdd = frag.substringAfter('?', "").addParameter() ?: getBooleanQueryParameter("add", false) return if (shouldAdd) url.newBuilder().also { wb -> wb.lora_config = null }.build() else url } @@ -149,9 +149,10 @@ enum class BeaconJoinOption { /** * Decides whether a beacon can be joined by simply **adding** its channel (no reboot) or requires a **switch** * (retune + reboot). Adding works only when the offered mesh sits on the radio's *current* frequency slot — Meshtastic - * secondary channels ride the primary channel's frequency, so the offered channel must resolve (name-hash) to the same - * slot the radio's primary is on, under a matching preset and region. Mirrors the Apple `014-mesh-beacons` - * FR-016/FR-017 logic. + * secondary channels ride the primary channel's frequency, so the offered channel must resolve to the same slot the + * radio's primary is on, under a matching preset and region. The offered slot comes from + * [MeshBeacon.offer_frequency_slot] when the beacon advertises one and from the channel-name hash otherwise; see + * [advertisedFrequencySlot]. Mirrors the Apple `014-mesh-beacons` FR-016/FR-017 logic. * * @param currentLora The radio's current [LoRaConfig] (`null` → can't reason, so [SWITCH]). * @param currentChannels The radio's current channel settings, index 0 = primary. @@ -165,13 +166,16 @@ fun MeshBeacon.beaconJoinOption(currentLora: LoRaConfig?, currentChannels: List< // offer_region == UNSET (0) means "not offered"; only a set, differing region forces a switch. val regionMatches = offer_region == RegionCode.UNSET || offer_region == lora.region if (!presetMatches || !regionMatches) return BeaconJoinOption.SWITCH - // With an explicit slot override we can't compare the offered mesh's slot; be safe and switch. - if (lora.channel_num != 0 || lora.numChannels <= 0) return BeaconJoinOption.SWITCH + if (lora.numChannels <= 0) return BeaconJoinOption.SWITCH + val advertisedSlot = advertisedFrequencySlot(lora) + // Only an advertised slot makes a pinned radio comparable: our own channel_num describes where we sit, never + // where the offered mesh sits, so without one there is nothing to compare a pin against. + if (advertisedSlot == null && lora.channel_num != 0) return BeaconJoinOption.SWITCH // Hash the *effective* names: an empty channel name resolves to its preset display name ("LongFast", …), which is // what firmware hashes for the slot — comparing raw "" on both sides would misclassify an unnamed primary. val currentSlot = lora.channelNum(Channel(currentChannels.firstOrNull() ?: ChannelSettings.Builder().build(), lora).name) - val offeredSlot = lora.channelNum(Channel(offer, lora).name) + val offeredSlot = advertisedSlot ?: lora.channelNum(Channel(offer, lora).name) return if (offeredSlot == currentSlot) BeaconJoinOption.ADD else BeaconJoinOption.SWITCH } @@ -180,13 +184,13 @@ fun MeshBeacon.beaconJoinOption(currentLora: LoRaConfig?, currentChannels: List< * * [ADD][BeaconJoinOption.ADD] omits `lora_config` so the dialog merges the offered channel into a free secondary slot * with no reboot. [SWITCH][BeaconJoinOption.SWITCH] carries a **fresh** `lora_config` (not a copy of [currentLora]) - * with `use_preset = true`, the advertised preset+region applied, and every RF field left at its default — notably - * `channel_num`/`override_frequency` zeroed so firmware re-derives the frequency from the offered channel name (Apple - * FR-006). Starting blank guarantees no stale slot/frequency pin (`channel_num`, `override_frequency`, manual - * bandwidth/spread_factor/coding_rate) from the old mesh survives the retune. Only `region` is carried from - * [currentLora] when the beacon doesn't advertise one — this config is sent as a full LoRaConfig replacement, and a - * zero region disables transmit; `hop_limit`/`tx_enabled` fall back to the app's standard defaults. Returns `null` for - * [NONE][BeaconJoinOption.NONE] or a beacon with no offered channel. + * with `use_preset = true`, the advertised preset+region applied, and every RF field left at its default. Starting + * blank guarantees no stale slot/frequency pin (`channel_num`, `override_frequency`, manual + * bandwidth/spread_factor/coding_rate) from the old mesh survives the retune. `channel_num` is then set to the slot the + * beacon advertises, or left zero so firmware re-derives it from the offered channel name (Apple FR-006) when it + * advertises none. Only `region` is carried from [currentLora] when the beacon doesn't advertise one — this config is + * sent as a full LoRaConfig replacement, and a zero region disables transmit; `hop_limit`/`tx_enabled` fall back to the + * app's standard defaults. Returns `null` for [NONE][BeaconJoinOption.NONE] or a beacon with no offered channel. * * Both paths strip position sharing from the offered channel ([withoutPositionSharing]) so joining a stranger's mesh * never leaks our location — matching Apple's `joinBeaconMesh`/`addBeaconChannel`. @@ -202,10 +206,9 @@ fun MeshBeacon.toJoinChannelSet(option: BeaconJoinOption, currentLora: LoRaConfi // Start from the shared device default ([Channel.default] — use_preset=true, hop_limit/tx_enabled set, // every RF field blank) rather than a copy of the current config: any stale RF pin on the connected radio // — an explicit channel_num, override_frequency, or manual bandwidth/spread_factor/coding_rate — would - // otherwise strand it on the old slot. use_preset + the default (zero) channel_num/override_frequency lets - // firmware re-derive the frequency from the offered channel name (Apple FR-006). This is sent as a full - // LoRaConfig replacement, so carry region (a zero region disables transmit); the beacon proto advertises - // no other RF fields. + // otherwise strand it on the old slot. This is sent as a full LoRaConfig replacement, so carry region (a + // zero region disables transmit); preset, region and the frequency slot are the only RF fields the beacon + // proto advertises. val base = currentLora ?: LoRaConfig.Builder().build() val loraConfig = Channel.default.loraConfig @@ -215,10 +218,13 @@ fun MeshBeacon.toJoinChannelSet(option: BeaconJoinOption, currentLora: LoRaConfi wb.region = if (offer_region != RegionCode.UNSET) offer_region else base.region } .build() + // Bound the advertised slot against the config we are about to send, not the one we are leaving: the + // offered region can have a different slot count from ours. Zero leaves firmware to derive it. + val advertisedSlot = advertisedFrequencySlot(loraConfig) ChannelSet.Builder() .also { wb -> wb.settings = listOf(offerChannel) - wb.lora_config = loraConfig + wb.lora_config = loraConfig.newBuilder().also { lb -> lb.channel_num = advertisedSlot ?: 0 }.build() } .build() } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt index 5315acff68..a84c860f44 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt @@ -129,7 +129,7 @@ fun normalizeReplacementSettings( } /** True when these settings carry no name and no PSK, making them padding rather than an intended channel. */ -fun ChannelSettings.isChannelPlaceholder(): Boolean = name.isNullOrBlank() && psk.size == 0 +fun ChannelSettings.isChannelPlaceholder(): Boolean = name.isBlank() && psk.size == 0 /** Semantic channel identity based on the effective name and effective PSK. */ data class ChannelIdentity(val name: String, val psk: ByteString) { diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DateTimeUtils.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DateTimeUtils.kt index e7e235656a..00f9c7bc5b 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DateTimeUtils.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DateTimeUtils.kt @@ -18,7 +18,6 @@ package org.meshtastic.core.model.util import org.meshtastic.core.model.util.TimeConstants.HOURS_PER_DAY import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds /** * Returns a short string representing the time if it's within the last 24 hours, otherwise returns a combined short @@ -29,26 +28,6 @@ import kotlin.time.Duration.Companion.seconds */ expect fun getShortDateTime(time: Long): String -/** - * Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s"). - * - * @param seconds The duration in seconds. - * @return A formatted uptime string. - */ -fun formatUptime(seconds: Int): String { - val secs = seconds.toLong() - if (secs == 0L) return "0s" - return secs.seconds.toComponents { days, hours, minutes, s, _ -> - listOfNotNull( - "${days}d".takeIf { days > 0 }, - "${hours}h".takeIf { hours > 0 }, - "${minutes}m".takeIf { minutes > 0 }, - "${s}s".takeIf { s > 0 }, - ) - .joinToString(" ") - } -} - /** * Calculates the remaining mute time in days and hours. * diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt index 9a6a146845..074ec971d8 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt @@ -41,34 +41,28 @@ fun Any?.anonymize(maxLen: Int = 3) = if (this != null) "...${this.toString().ta // A toString that makes sure all newlines are removed (for nice logging). fun Any.toOneLineString() = this.toString().replace('\n', ' ') -fun Config.toOneLineString(): String { - // Wire toString uses field=value format - val redactedFields = """(wifi_psk|public_key|private_key|admin_key)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} +// Wire toString uses field=value format. Compiled once: these run for every config frame. +private val CONFIG_SECRETS = Regex("""(wifi_psk|public_key|private_key|admin_key)=[^,}]+""") +private val MESH_PACKET_SECRETS = Regex("""(public_key|private_key|admin_key)=[^,}]+""") +private val CHANNEL_SECRETS = Regex("""(psk)=[^,}]+""") +private val MODULE_CONFIG_SECRETS = Regex("""(password|username)=[^,}]+""") +private val MY_NODE_INFO_SECRETS = Regex("""(device_id)=[^,}]+""") -fun MeshPacket.toOneLineString(): String { - val redactedFields = """(public_key|private_key|admin_key)=[^,}]+""" // Redact keys - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} +private fun String.redactOneLine(fields: Regex): String = + replace(fields) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -fun Channel.toOneLineString(): String { - // Redact the channel preshared key (psk) from logs. - val redactedFields = """(psk)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} +fun Config.toOneLineString(): String = toString().redactOneLine(CONFIG_SECRETS) -fun ModuleConfig.toOneLineString(): String { - // Redact MQTT credentials from logs. - val redactedFields = """(password|username)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} +fun MeshPacket.toOneLineString(): String = toString().redactOneLine(MESH_PACKET_SECRETS) -fun MyNodeInfo.toOneLineString(): String { - // Redact the hardware unique identifier from logs. - val redactedFields = """(device_id)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} +/** Redacts the channel preshared key. */ +fun Channel.toOneLineString(): String = toString().redactOneLine(CHANNEL_SECRETS) + +/** Redacts MQTT credentials. */ +fun ModuleConfig.toOneLineString(): String = toString().redactOneLine(MODULE_CONFIG_SECRETS) + +/** Redacts the hardware unique identifier. */ +fun MyNodeInfo.toOneLineString(): String = toString().redactOneLine(MY_NODE_INFO_SECRETS) fun Any.toPIIString() = if (!isDebug) { "" @@ -76,9 +70,6 @@ fun Any.toPIIString() = if (!isDebug) { this.toOneLineString() } -@Suppress("MagicNumber") -fun ByteArray.toHexString() = joinToString("") { it.toUByte().toString(16).padStart(2, '0') } - /** Returns true if this packet arrived via a LoRa transport mechanism. */ fun MeshPacket.isLora(): Boolean = transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA || transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT1 || @@ -119,7 +110,7 @@ fun MeshPacket.isDirectSignal(): Boolean = */ fun Telemetry.hasValidEnvironmentMetrics(): Boolean { val metrics = this.environment_metrics ?: return false - val hasClimate = metrics.relative_humidity != null && metrics.temperature != null && !metrics.temperature!!.isNaN() + val hasClimate = metrics.relative_humidity != null && metrics.temperature?.isNaN() == false val hasLightning = metrics.lightning_strike_count_1h != null || metrics.lightning_distance_km?.isNaN() == false return hasClimate || hasLightning } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/GeoConstants.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/GeoConstants.kt index 252297754a..d330410f80 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/GeoConstants.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/GeoConstants.kt @@ -23,7 +23,4 @@ object GeoConstants { /** Multiplier to convert protobuf integer heading values (1e-5 degree units) to decimal degrees. */ const val HEADING_DEG = 1e-5 - - /** Mean radius of the Earth in meters, for haversine calculations. */ - const val EARTH_RADIUS_METERS = 6_371_000.0 } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/HostAnonymize.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/HostAnonymize.kt new file mode 100644 index 0000000000..4066130747 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/HostAnonymize.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.core.model.util + +/** + * [anonymize] for a TCP `host`, `host:port` or `[ipv6]:port`, except that a host which can only name a machine on the + * user's own network is returned whole: loopback, RFC 1918, IPv4 and IPv6 link-local, IPv6 unique local, or an mDNS + * `.local` name. A public address or DNS name, DDNS and MagicDNS included, can identify the user, so it is anonymized. + */ +fun String.anonymizePublicHost(): String = if (isLocalNetworkHost(hostOf(this))) this else anonymize() + +private fun hostOf(address: String): String { + val host = + when { + address.startsWith("[") -> address.substringAfter('[').substringBefore(']') + address.count { it == ':' } == 1 -> address.substringBefore(':') + else -> address + } + return host.substringBefore('%').trimEnd('.').lowercase() +} + +private fun isLocalNetworkHost(host: String): Boolean = + host == "localhost" || host.endsWith(".local") || isLocalIpv4(host) || isLocalIpv6(host) + +private fun isLocalIpv4(host: String): Boolean { + val parts = host.split('.') + val octets = parts.mapNotNull { part -> part.toIntOrNull()?.takeIf { it in 0..255 } } + if (parts.size != 4 || octets.size != 4) return false + val (first, second) = octets + return when (first) { + 127, + 10, + -> true + + 172 -> second in 16..31 + + 192 -> second == 168 + + 169 -> second == 254 + + else -> false + } +} + +private fun isLocalIpv6(host: String): Boolean { + val isLiteral = host.count { it == ':' } >= 2 && host.all { it == ':' || it.digitToIntOrNull(16) != null } + val firstHextet = host.substringBefore(':').takeIf { it.length in 1..4 }?.toIntOrNull(16) + // fe80::/10 link-local, fc00::/7 unique local. + val isLocalRange = firstHextet != null && (firstHextet in 0xfe80..0xfebf || firstHextet in 0xfc00..0xfdff) + return isLiteral && (host == "::1" || isLocalRange) +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MeshBeaconSlot.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MeshBeaconSlot.kt new file mode 100644 index 0000000000..ec6c4510bc --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/MeshBeaconSlot.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model.util + +import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.numChannels +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config.LoRaConfig +import org.meshtastic.proto.MeshBeacon + +// Both ends of a Mesh Beacon's frequency slot: what an incoming offer says it sits on, and what an outgoing offer +// should say about us. A slot is 1-based to match LoRaConfig.channel_num and 0 means "not sent". + +/** + * The frequency slot this beacon advertises outright, or `null` when it advertises none and the slot must be derived + * from the offered region, channel name and preset the way [LoRaConfig.channelNum] does. + * + * A mesh sends this only when derivation would produce the wrong answer — it pins a slot the offered name does not hash + * to. A value outside [lora]'s slot count is unaddressable here, so it falls back to derivation rather than refusing + * the join: that is the behaviour we had before the field existed, and the offer is advisory either way. + */ +internal fun MeshBeacon.advertisedFrequencySlot(lora: LoRaConfig): Int? = + offer_frequency_slot?.takeIf { it in 1..lora.numChannels } + +/** + * The frequency slot to advertise alongside an outgoing offer, or `null` to leave the field unset. + * + * Firmware sends this on the air only where it differs from the slot a receiver derives from the offered region, + * channel name and preset, so stamping the radio's real slot costs nothing until the two diverge. They diverge in + * exactly the cases the field exists for: a radio pinned to an explicit `channel_num`, a region that mandates a slot, + * or an offered channel whose name hashes somewhere other than where the radio actually sits. Without this the radio + * advertises a frequency it is not on, and a client that joins hears nothing. + */ +fun beaconOfferFrequencySlot( + offerChannel: ChannelSettings?, + primaryChannel: ChannelSettings?, + radioLora: LoRaConfig, +): Int? { + if (offerChannel == null || radioLora.numChannels <= 0) return null + val actual = Channel(primaryChannel ?: ChannelSettings.Builder().build(), radioLora).channelNum + // Derive the way a receiver must: off the offered channel's name with our own pin removed. + val derived = Channel(offerChannel, radioLora.newBuilder().also { wb -> wb.channel_num = 0 }.build()).channelNum + return actual.takeIf { it != derived } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt index e9e304573b..0043ed7fdc 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt @@ -48,13 +48,13 @@ fun Node.toSharedContact(isOwnContact: Boolean = false): SharedContact = SharedC */ @Throws(MalformedMeshtasticUrlException::class) fun CommonUri.toSharedContact(): SharedContact { - checkSharedContactUrl() - val data = fragment!!.substringBefore('?') + val data = sharedContactFragment().substringBefore('?') return decodeSharedContactData(data) } +/** The fragment of a shared-contact URL, after checking the host and path. */ @Throws(MalformedMeshtasticUrlException::class) -private fun CommonUri.checkSharedContactUrl() { +private fun CommonUri.sharedContactFragment(): String { val h = host?.lowercase() ?: "" val isCorrectHost = h == MESHTASTIC_HOST || h == "www.$MESHTASTIC_HOST" val segments = pathSegments @@ -66,6 +66,7 @@ private fun CommonUri.checkSharedContactUrl() { "Not a valid Meshtastic URL: host=$h, segmentCount=${segments.size}, hasFragment=${!frag.isNullOrBlank()}", ) } + return frag } @Suppress("ThrowsCount") diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TimeConstants.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TimeConstants.kt index c573927d23..25f005fc81 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TimeConstants.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TimeConstants.kt @@ -27,5 +27,7 @@ object TimeConstants { val TWO_DAYS = 2.days const val HOURS_PER_DAY = 24 + const val SECONDS_PER_MINUTE = 60 + const val SECONDS_PER_HOUR = 3600 const val MS_PER_SEC = 1000L } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/AckProofStatusTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/AckProofStatusTest.kt new file mode 100644 index 0000000000..4f23bbc715 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/AckProofStatusTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.message_status_ack_proof_invalid +import org.meshtastic.core.resources.message_status_recipient_delivered +import org.meshtastic.core.resources.message_status_recipient_delivered_proven +import org.meshtastic.proto.MeshPacket +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class AckProofStatusTest { + + @Test + fun `an absent proof shows nothing extra`() { + assertNull(getAckProofStatusRes(MeshPacket.AckProofStatus.ACK_PROOF_ABSENT.value)) + assertFalse(isAckProofForged(MeshPacket.AckProofStatus.ACK_PROOF_ABSENT.value)) + } + + @Test + fun `an unknown value reads as absent`() { + assertNull(getAckProofStatusRes(99)) + assertFalse(isAckProofForged(99)) + } + + @Test + fun `every carried proof gets its own row whatever the verdict`() { + listOf( + MeshPacket.AckProofStatus.ACK_PROOF_VALID, + MeshPacket.AckProofStatus.ACK_PROOF_INVALID, + MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY, + ) + .forEach { assertNotNull(getAckProofStatusRes(it.value), "no row for $it") } + } + + @Test + fun `only a failed proof counts as forged`() { + assertTrue(isAckProofForged(MeshPacket.AckProofStatus.ACK_PROOF_INVALID.value)) + assertFalse(isAckProofForged(MeshPacket.AckProofStatus.ACK_PROOF_VALID.value)) + assertFalse(isAckProofForged(MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY.value)) + } + + @Test + fun `a proven ack is the only delivery the status line calls proven`() { + val proven = + getMessageStatusStringRes( + status = MessageStatus.RECEIVED, + routingError = 0, + isDirectMessage = true, + ackProofStatus = MeshPacket.AckProofStatus.ACK_PROOF_VALID.value, + ) + assertEquals(Res.string.message_status_recipient_delivered_proven, proven.second) + + listOf(MeshPacket.AckProofStatus.ACK_PROOF_ABSENT, MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY).forEach { + val unproven = + getMessageStatusStringRes( + status = MessageStatus.RECEIVED, + routingError = 0, + isDirectMessage = true, + ackProofStatus = it.value, + ) + assertEquals(Res.string.message_status_recipient_delivered, unproven.second, "wrong text for $it") + } + } + + @Test + fun `a failed proof replaces the delivery text rather than reading as delivered`() { + val forged = + getMessageStatusStringRes( + status = MessageStatus.RECEIVED, + routingError = 0, + isDirectMessage = true, + ackProofStatus = MeshPacket.AckProofStatus.ACK_PROOF_INVALID.value, + ) + assertEquals(Res.string.message_status_ack_proof_invalid, forged.second) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt index b57dd8edeb..d8232c63f3 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt @@ -16,8 +16,10 @@ */ package org.meshtastic.core.model +import org.meshtastic.proto.FieldMetadata import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -32,6 +34,12 @@ class CapabilitiesTest { assertTrue(caps("2.8.0").canMuteNode) } + @Test + fun supportsCodingRateOverride_requires_V2_7_18() { + assertFalse(caps("2.7.17").supportsCodingRateOverride) + assertTrue(caps("2.7.18").supportsCodingRateOverride) + } + @Test fun canRequestNeighborInfo_is_currently_disabled() { assertFalse(caps("2.7.14").canRequestNeighborInfo) @@ -50,12 +58,6 @@ class CapabilitiesTest { assertTrue(caps("2.7.12").canSendVerifiedContacts) } - @Test - fun canToggleTelemetryEnabled_requires_V2_7_12() { - assertFalse(caps("2.7.11").canToggleTelemetryEnabled) - assertTrue(caps("2.7.12").canToggleTelemetryEnabled) - } - @Test fun canToggleUnmessageable_requires_V2_6_9() { assertFalse(caps("2.6.8").canToggleUnmessageable) @@ -75,9 +77,47 @@ class CapabilitiesTest { } @Test - fun supportsStatusMessage_requires_V2_8_0() { - assertFalse(caps("2.7.21").supportsStatusMessage) - assertTrue(caps("2.8.0").supportsStatusMessage) + fun supportsStatusMessage_requires_V2_7_20() { + assertFalse(caps("2.7.19").supportsStatusMessage) + assertTrue(caps("2.7.20").supportsStatusMessage) + } + + @Test + fun offers_hides_a_field_below_since_firmware() { + val field = FieldMetadata.Builder().since_firmware("2.7.13").build() + assertFalse(caps("2.7.12").offers(field)) + assertTrue(caps("2.7.13").offers(field)) + assertFalse(caps(null).offers(field)) + } + + @Test + fun offers_hides_a_deprecated_field_at_deprecated_since_unless_set() { + val field = FieldMetadata.Builder().deprecated_since("2.8.0").build() + assertTrue(caps("2.7.26").offers(field)) + assertFalse(caps("2.8.0").offers(field)) + assertTrue(caps("2.8.0").offers(field, isSet = true)) + // Unknown firmware cannot be shown to have retired the field. + assertTrue(caps(null).offers(field)) + } + + @Test + fun offers_an_unannotated_field_everywhere() { + val field = FieldMetadata.Builder().build() + assertTrue(caps(null).offers(field)) + assertTrue(caps("2.3.15").offers(field)) + } + + @Test + fun offers_everything_when_forceEnableAll() { + val c = Capabilities(firmwareVersion = null, forceEnableAll = true) + assertTrue(c.offers(FieldMetadata.Builder().since_firmware("9.9.9").build())) + assertTrue(c.offers(FieldMetadata.Builder().deprecated_since("1.0.0").build())) + } + + @Test + fun offers_rejects_an_unparseable_schema_version() { + val field = FieldMetadata.Builder().since_firmware("soon").build() + assertFailsWith { caps("2.8.0").offers(field) } } @Test @@ -164,7 +204,6 @@ class CapabilitiesTest { assertFalse(c.canMuteNode) assertFalse(c.canRequestNeighborInfo) assertFalse(c.canSendVerifiedContacts) - assertFalse(c.canToggleTelemetryEnabled) assertFalse(c.canToggleUnmessageable) assertFalse(c.supportsQrCodeSharing) assertFalse(c.supportsSecondaryChannelLocation) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt index 652e62c079..66f074091b 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt @@ -16,10 +16,12 @@ */ package org.meshtastic.core.model +import org.meshtastic.proto.Config.LoRaConfig import org.meshtastic.proto.Config.LoRaConfig.ModemPreset import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue class ChannelOptionTest { @@ -67,4 +69,59 @@ class ChannelOptionTest { "Each ChannelOption must map to a unique ModemPreset.", ) } + + @Test + fun a_preset_offers_only_the_coding_rates_above_its_own() { + assertEquals(6..8, ChannelOption.LONG_FAST.codingRateOverrides) + assertEquals(7..8, ChannelOption.NARROW_FAST.codingRateOverrides) + assertTrue(ChannelOption.LONG_SLOW.codingRateOverrides.isEmpty()) + } + + @Test + fun a_stored_coding_rate_the_preset_already_meets_is_the_preset_default() { + assertEquals(0, ChannelOption.LONG_FAST.codingRateOverride(5)) + assertEquals(0, ChannelOption.LONG_FAST.codingRateOverride(0)) + assertEquals(0, ChannelOption.LONG_FAST.codingRateOverride(9)) + assertEquals(0, ChannelOption.NARROW_FAST.codingRateOverride(6)) + assertEquals(0, ChannelOption.LONG_SLOW.codingRateOverride(8)) + assertEquals(7, ChannelOption.LONG_FAST.codingRateOverride(7)) + } + + @Test + fun the_effective_coding_rate_is_the_override_or_else_the_preset() { + assertEquals(7, ChannelOption.LONG_FAST.effectiveCodingRate(7)) + assertEquals(5, ChannelOption.LONG_FAST.effectiveCodingRate(0)) + assertEquals(6, ChannelOption.NARROW_FAST.effectiveCodingRate(5)) + } + + @Test + fun normalizing_keeps_an_override_that_still_raises_the_new_preset() { + assertEquals(7, presetConfig(ModemPreset.MEDIUM_FAST, codingRate = 7).normalizeCodingRateOverride().coding_rate) + } + + @Test + fun normalizing_resets_an_override_the_preset_already_meets() { + assertEquals(0, presetConfig(ModemPreset.LONG_SLOW, codingRate = 7).normalizeCodingRateOverride().coding_rate) + assertEquals(0, presetConfig(ModemPreset.LONG_FAST, codingRate = 5).normalizeCodingRateOverride().coding_rate) + } + + @Test + fun normalizing_leaves_a_manual_config_alone() { + val manual = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = false + wb.coding_rate = 5 + } + .build() + assertEquals(manual, manual.normalizeCodingRateOverride()) + } + + private fun presetConfig(preset: ModemPreset, codingRate: Int) = LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = preset + wb.coding_rate = codingRate + } + .build() } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceAddressTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceAddressTest.kt new file mode 100644 index 0000000000..37e08487fe --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceAddressTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNull + +class DeviceAddressTest { + + @Test + fun `every no-device spelling parses to no selection`() { + listOf(null, "", " ", "n", "N", "null", ".n", "default", "DEFAULT").forEach { raw -> + assertNull(DeviceAddress.parse(raw), "expected no selection for '$raw'") + } + } + + @Test + fun `an unknown prefix parses to no selection`() { + assertNull(DeviceAddress.parse("z12:34")) + } + + @Test + fun `each prefix names its transport and keeps the rest as identity`() { + val cases = + mapOf( + "x11:22:33:44:55:66" to InterfaceId.BLUETOOTH, + "s1027:29987:0" to InterfaceId.SERIAL, + "t192.168.1.20:4403" to InterfaceId.TCP, + "m" to InterfaceId.MOCK, + "r" to InterfaceId.REPLAY, + ) + cases.forEach { (raw, interfaceId) -> + val address = checkNotNull(DeviceAddress.parse(raw)) + assertEquals(interfaceId, address.interfaceId) + assertEquals(raw, address.raw) + assertEquals(raw.drop(1), address.identity) + } + } + + @Test + fun `the legacy bang prefix reads as BLE and keeps its stored form`() { + val address = checkNotNull(DeviceAddress.parse("!11:22:33:44:55:66")) + + assertEquals(InterfaceId.BLUETOOTH, address.interfaceId) + assertEquals("!11:22:33:44:55:66", address.raw) + assertEquals("11:22:33:44:55:66", address.identity) + } + + @Test + fun `addresses are equal only when their stored form is`() { + assertEquals(DeviceAddress.parse("xAA"), DeviceAddress.parse("xAA")) + assertNotEquals(DeviceAddress.parse("xAA"), DeviceAddress.parse("!AA")) + } + + @Test + fun `toString does not reveal the identity`() { + assertEquals("DeviceAddress(BLUETOOTH)", DeviceAddress.parse("x11:22:33:44:55:66").toString()) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceTypeTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceTypeTest.kt index 91320bd493..22b7712da9 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceTypeTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/DeviceTypeTest.kt @@ -26,12 +26,12 @@ class DeviceTypeTest { fun fromAddress_preserves_transport_analytics_names() { assertEquals("BLE", DeviceType.fromAddress("x123")?.name) assertEquals("USB", DeviceType.fromAddress("s/dev/bus/usb/001/002")?.name) - assertEquals("USB", DeviceType.fromAddress("m")?.name) assertEquals("TCP", DeviceType.fromAddress("t192.0.2.1")?.name) } @Test fun fromAddress_returns_null_for_non_presented_prefixes() { + assertNull(DeviceType.fromAddress("m")) assertNull(DeviceType.fromAddress("r")) assertNull(DeviceType.fromAddress("n")) assertNull(DeviceType.fromAddress("")) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ExcludedModuleTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ExcludedModuleTest.kt new file mode 100644 index 0000000000..9e8a0f0e71 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ExcludedModuleTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.ExcludedModules +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ExcludedModuleTest { + + private fun metadata(excluded: Int) = DeviceMetadata.Builder().also { wb -> wb.excluded_modules = excluded }.build() + + @Test + fun `metadata not read yet excludes nothing`() { + val none: DeviceMetadata? = null + + assertFalse(none.excludes(ExcludedModules.MQTT_CONFIG)) + assertFalse(none.excludes(ExcludedModules.STATUSMESSAGE_CONFIG)) + } + + @Test + fun `a set bit excludes only its own module`() { + val metadata = metadata(ExcludedModules.STATUSMESSAGE_CONFIG.value or ExcludedModules.TAK_CONFIG.value) + + assertTrue(metadata.excludes(ExcludedModules.STATUSMESSAGE_CONFIG)) + assertTrue(metadata.excludes(ExcludedModules.TAK_CONFIG)) + assertFalse(metadata.excludes(ExcludedModules.MESHBEACON_CONFIG)) + assertFalse(metadata.excludes(ExcludedModules.MQTT_CONFIG)) + } + + @Test + fun `no bits set excludes nothing`() { + assertFalse(metadata(ExcludedModules.EXCLUDED_NONE.value).excludes(ExcludedModules.MESHBEACON_CONFIG)) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/HardwareSupportTierTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/HardwareSupportTierTest.kt new file mode 100644 index 0000000000..b6d17b900f --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/HardwareSupportTierTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import kotlin.test.Test +import kotlin.test.assertEquals + +class HardwareSupportTierTest { + + private fun hardware(activelySupported: Boolean, isMaker: Boolean, supportLevel: Int?) = + DeviceHardware(activelySupported = activelySupported, isMaker = isMaker, supportLevel = supportLevel) + + @Test + fun `a maker board reads maker even once the registry marks it actively supported`() { + assertEquals(HardwareSupportTier.MAKER, hardware(false, true, 1).supportTier) + assertEquals(HardwareSupportTier.MAKER, hardware(true, true, 1).supportTier) + assertEquals(HardwareSupportTier.MAKER, hardware(true, true, 2).supportTier) + } + + @Test + fun `actively supported decides the rung for everything that is not maker`() { + assertEquals(HardwareSupportTier.SUPPORTED, hardware(true, false, 1).supportTier) + assertEquals(HardwareSupportTier.SUPPORTED, hardware(true, false, 3).supportTier) + assertEquals(HardwareSupportTier.SUPPORTED, hardware(true, false, null).supportTier) + assertEquals(HardwareSupportTier.COMMUNITY, hardware(false, false, 1).supportTier) + assertEquals(HardwareSupportTier.COMMUNITY, hardware(false, false, null).supportTier) + } + + @Test + fun `a legacy or untiered maker board falls through to the lifecycle flag`() { + assertEquals(HardwareSupportTier.SUPPORTED, hardware(true, true, 3).supportTier) + assertEquals(HardwareSupportTier.COMMUNITY, hardware(false, true, 3).supportTier) + assertEquals(HardwareSupportTier.COMMUNITY, hardware(false, true, null).supportTier) + } + + @Test + fun `isMaker defaults to false so an absent key reads as not maker`() { + assertEquals(false, DeviceHardware().isMaker) + assertEquals(false, NetworkDeviceHardware().isMaker) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt index 21646de772..7c3adc4f4c 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt @@ -72,6 +72,16 @@ class NodeTelemetryStringsTest { assertEquals(listOf("0.0°C", "0%"), strings) } + @Test + fun zero_iaq_is_reported() { + assertEquals(listOf("IAQ: 0"), telemetry(EnvironmentMetrics.Builder().also { wb -> wb.iaq = 0 }.build())) + } + + @Test + fun measured_iaq_is_reported() { + assertEquals(listOf("IAQ: 57"), telemetry(EnvironmentMetrics.Builder().also { wb -> wb.iaq = 57 }.build())) + } + @Test fun soil_moisture_no_longer_requires_a_soil_temperature() { assertEquals( diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt index efa412f0cb..1ab89146ad 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt @@ -198,6 +198,25 @@ class NodeTest { assertTrue(Node(num = 1).matchesSearch("")) } + @Test + fun matchesSearch_matchesTheStatusMessage() { + val node = + Node( + num = 1, + user = User.Builder().also { wb -> wb.long_name = "Summit Repeater" }.build(), + nodeStatus = "På fjellet", + ) + + assertTrue(node.matchesSearch("fjellet")) + assertTrue(node.matchesSearch("PÅ FJELLET")) + assertFalse(node.matchesSearch("nomatch")) + } + + @Test + fun matchesSearch_toleratesAnAbsentStatusMessage() { + assertFalse(Node(num = 1, nodeStatus = null).matchesSearch("anything")) + } + private fun nodeWithPosition(num: Int, latitudeI: Int, longitudeI: Int): Node = Node( num = num, position = diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/PositionFixTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/PositionFixTest.kt new file mode 100644 index 0000000000..d86aca89d1 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/PositionFixTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.meshtastic.proto.Position as ProtoPosition + +class PositionFixTest { + + private fun position(latitude: Int?, longitude: Int?) = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = latitude + wb.longitude_i = longitude + wb.time = 1_700_000_000 + } + .build() + + @Test + fun `a position without latitude has no fix`() { + val position = position(latitude = null, longitude = 134_050_000) + + assertNull(position.fixOrNull()) + assertFalse(position.hasFix()) + } + + @Test + fun `a position without longitude has no fix`() { + val position = position(latitude = 525_200_000, longitude = null) + + assertNull(position.fixOrNull()) + assertFalse(position.hasFix()) + } + + @Test + fun `a position at zero latitude and zero longitude has no fix`() { + val position = position(latitude = 0, longitude = 0) + + assertNull(position.fixOrNull()) + assertFalse(position.hasFix()) + } + + @Test + fun `a position on the equator has a fix`() { + val position = position(latitude = 0, longitude = 134_050_000) + + assertEquals(0 to 134_050_000, position.fixOrNull()) + assertTrue(position.hasFix()) + } + + @Test + fun `a position on the prime meridian has a fix`() { + val position = position(latitude = 525_200_000, longitude = 0) + + assertEquals(525_200_000 to 0, position.fixOrNull()) + assertTrue(position.hasFix()) + } + + @Test + fun `a position with both axes has a fix`() { + val position = position(latitude = 525_200_000, longitude = 134_050_000) + + assertEquals(525_200_000 to 134_050_000, position.fixOrNull()) + assertTrue(position.hasFix()) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RouteDiscoveryTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RouteDiscoveryTest.kt index a89f2b8866..402d46608e 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RouteDiscoveryTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RouteDiscoveryTest.kt @@ -23,7 +23,6 @@ import kotlin.test.assertEquals * Tests for [evaluateTracerouteMapAvailability] — the pure function that determines whether a traceroute can be * visualised on a map based on node position data. */ -@Suppress("MagicNumber") class RouteDiscoveryTest { @Test diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/HostAnonymizeTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/HostAnonymizeTest.kt new file mode 100644 index 0000000000..fbce2ecaf5 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/HostAnonymizeTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model.util + +import kotlin.test.Test +import kotlin.test.assertEquals + +class HostAnonymizeTest { + + private fun assertKept(vararg hosts: String) { + for (host in hosts) assertEquals(host, host.anonymizePublicHost(), "expected $host to stay readable") + } + + private fun assertAnonymized(vararg hosts: String) { + for (host in hosts) assertEquals(host.anonymize(), host.anonymizePublicHost(), "expected $host anonymized") + } + + @Test + fun `loopback hosts stay readable`() { + assertKept("127.0.0.1", "127.0.0.1:4403", "localhost", "::1", "[::1]:4403") + } + + @Test + fun `RFC 1918 private IPv4 hosts stay readable`() { + assertKept("10.0.0.5", "172.16.0.1", "172.31.255.254:4403", "192.168.1.50", "192.168.1.50:4403") + } + + @Test + fun `addresses just outside the RFC 1918 blocks are anonymized`() { + assertAnonymized("172.15.0.1", "172.32.0.1", "11.0.0.1", "192.169.1.1") + } + + @Test + fun `IPv4 and IPv6 link-local hosts stay readable`() { + assertKept("169.254.10.20", "fe80::1", "febf::1", "[fe80::1%en0]:4403") + } + + @Test + fun `IPv6 unique local hosts stay readable`() { + assertKept("fc00::1", "fd12:3456:789a::1", "[fdab::2]:4403") + } + + @Test + fun `IPv6 addresses outside the local blocks are anonymized`() { + assertAnonymized("fec0::1", "2001:db8::1", "[2606:4700::1111]:4403") + } + + @Test + fun `mDNS local names stay readable`() { + assertKept("meshtastic.local", "Meshtastic.local:4403", "node.local.") + } + + @Test + fun `public IPv4 addresses are anonymized`() { + assertAnonymized("8.8.8.8", "8.8.8.8:4403", "203.0.113.7") + } + + @Test + fun `DDNS and MagicDNS names are anonymized`() { + assertAnonymized("mynode.duckdns.org", "node.tail1234.ts.net:4403", "meshnode") + } + + @Test + fun `malformed IPv4 literals are anonymized`() { + assertAnonymized("256.1.1.1", "192.168.1", "10.0.0.1.5") + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt index d7968dc53a..d82f4cc0fb 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt @@ -20,6 +20,7 @@ import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.Channel import org.meshtastic.core.model.MeshBeaconOffer +import org.meshtastic.core.model.numChannels import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config.LoRaConfig import org.meshtastic.proto.Config.LoRaConfig.ModemPreset @@ -422,4 +423,70 @@ class MeshBeaconOfferTest { .build() assertEquals(false, beacon.isAlreadyJoined(radioLora, configured)) } + + private fun slotOf(name: String, lora: LoRaConfig = radioLora): Int = + Channel(ChannelSettings.Builder().also { wb -> wb.name = name }.build(), lora).channelNum + + private fun offerOn(name: String, slot: Int? = null): MeshBeacon = MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { cb -> cb.name = name }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + wb.offer_frequency_slot = slot + } + .build() + + @Test + fun `an advertised slot is honoured over the offered channel name hash`() { + // A mesh that pins a slot its channel name does not hash to can only be described by the advertised slot. + val homeSlot = slotOf("HomeMesh") + val offered = generateSequence(0) { it + 1 }.map { "PinnedMesh$it" }.first { slotOf(it) != homeSlot } + assertEquals(BeaconJoinOption.ADD, offerOn(offered, slot = homeSlot).beaconJoinOption(radioLora, radioChannels)) + // Same offer without the slot derives elsewhere, so it is only addable because the slot was advertised. + assertEquals(BeaconJoinOption.SWITCH, offerOn(offered).beaconJoinOption(radioLora, radioChannels)) + } + + @Test + fun `an advertised slot away from ours forces SWITCH even when the names hash alike`() { + // The NYMesh case: the offered name hashes to our slot, but the mesh actually sits elsewhere. Deriving would + // call this a no-reboot ADD and the user would join and hear nothing. + val homeSlot = slotOf("HomeMesh") + val elsewhere = if (homeSlot < radioLora.numChannels) homeSlot + 1 else homeSlot - 1 + assertEquals( + BeaconJoinOption.SWITCH, + offerOn("HomeMesh", slot = elsewhere).beaconJoinOption(radioLora, radioChannels), + ) + } + + @Test + fun `a pinned radio compares against an advertised slot instead of refusing to reason`() { + val pinned = radioLora.newBuilder().also { wb -> wb.channel_num = 48 }.build() + assertEquals(BeaconJoinOption.ADD, offerOn("AnyName", slot = 48).beaconJoinOption(pinned, radioChannels)) + assertEquals(BeaconJoinOption.SWITCH, offerOn("AnyName", slot = 49).beaconJoinOption(pinned, radioChannels)) + // With nothing advertised there is still nothing to compare a pin against. + assertEquals(BeaconJoinOption.SWITCH, offerOn("HomeMesh").beaconJoinOption(pinned, radioChannels)) + } + + @Test + fun `an advertised slot outside the region falls back to the name hash`() { + // Unaddressable here, so behave as though the field were absent rather than refuse an advisory offer. + assertEquals( + BeaconJoinOption.ADD, + offerOn("HomeMesh", slot = radioLora.numChannels + 1).beaconJoinOption(radioLora, radioChannels), + ) + assertEquals(BeaconJoinOption.ADD, offerOn("HomeMesh", slot = 0).beaconJoinOption(radioLora, radioChannels)) + } + + @Test + fun `a switch carries the advertised slot into channel_num`() { + val beacon = offerOn("PinnedMesh", slot = 48) + val set = assertNotNull(beacon.toJoinChannelSet(BeaconJoinOption.SWITCH, radioLora)) + assertEquals(48, assertNotNull(set.lora_config).channel_num) + } + + @Test + fun `a switch with no advertised slot leaves channel_num zero for firmware to derive`() { + val set = assertNotNull(offerOn("PinnedMesh").toJoinChannelSet(BeaconJoinOption.SWITCH, radioLora)) + assertEquals(0, assertNotNull(set.lora_config).channel_num) + } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt index 114cb634ae..389ec30d4f 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt @@ -21,7 +21,6 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.NodeAddress import org.meshtastic.proto.Config import org.meshtastic.proto.Data -import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.MeshPacket @@ -34,7 +33,6 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -import org.meshtastic.core.model.DeviceMetrics as DomainDeviceMetrics import org.meshtastic.core.model.EnvironmentMetrics as DomainEnvironmentMetrics import org.meshtastic.core.model.Position as DomainPosition @@ -219,12 +217,19 @@ class MeshDataMapperTest { } @Test - fun position_usesDefaultTimeAndZeroValuesForUnsetProtoFields() { + fun position_keepsSeaLevelAltitudeDistinctFromAbsent() { + val position = DomainPosition(Position.Builder().also { wb -> wb.altitude = 0 }.build(), defaultTime = 1) + + assertEquals(0, position.altitude) + } + + @Test + fun position_leavesAltitudeAbsentAndDefaultsOtherUnsetProtoFields() { val position = DomainPosition(Position.Builder().build(), defaultTime = 789) assertEquals(0.0, position.latitude) assertEquals(0.0, position.longitude) - assertEquals(0, position.altitude) + assertNull(position.altitude) assertEquals(789, position.time) assertEquals(0, position.satellitesInView) assertEquals(0, position.groundSpeed) @@ -232,41 +237,6 @@ class MeshDataMapperTest { assertEquals(0, position.precisionBits) } - @Test - fun deviceMetrics_mapsProtoFields() { - val proto = - DeviceMetrics.Builder() - .also { wb -> - wb.battery_level = 87 - wb.voltage = 4.12f - wb.channel_utilization = 32.5f - wb.air_util_tx = 7.75f - wb.uptime_seconds = 3600 - } - .build() - - val metrics = DomainDeviceMetrics(proto, telemetryTime = 123) - - assertEquals(123, metrics.time) - assertEquals(87, metrics.batteryLevel) - assertEquals(4.12f, metrics.voltage) - assertEquals(32.5f, metrics.channelUtilization) - assertEquals(7.75f, metrics.airUtilTx) - assertEquals(3600, metrics.uptimeSeconds) - } - - @Test - fun deviceMetrics_defaultsUnsetFieldsToZero() { - val metrics = DomainDeviceMetrics(DeviceMetrics.Builder().build(), telemetryTime = 222) - - assertEquals(222, metrics.time) - assertEquals(0, metrics.batteryLevel) - assertEquals(0f, metrics.voltage) - assertEquals(0f, metrics.channelUtilization) - assertEquals(0f, metrics.airUtilTx) - assertEquals(0, metrics.uptimeSeconds) - } - @Test fun environmentMetrics_mapsTelemetryFields() { val proto = diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/OneLineStringRedactionTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/OneLineStringRedactionTest.kt new file mode 100644 index 0000000000..22fb303392 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/OneLineStringRedactionTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model.util + +import okio.ByteString.Companion.toByteString +import org.meshtastic.proto.Channel +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config +import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.MyNodeInfo +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class OneLineStringRedactionTest { + + // Not valid UTF-8, so okio renders it as hex rather than text. + private val secret = ByteArray(16) { 0xFF.toByte() }.toByteString() + + @Test + fun `channel psk is redacted`() { + val channel = + Channel.Builder() + .also { wb -> + wb.settings = + ChannelSettings.Builder() + .also { cs -> + cs.psk = secret + cs.name = "Primary" + } + .build() + } + .build() + + val line = channel.toOneLineString() + + assertTrue(line.contains("psk=[REDACTED]"), line) + assertFalse(line.contains(secret.hex()), line) + assertTrue(line.contains("Primary"), line) + } + + @Test + fun `security keys are redacted from config`() { + val config = + Config.Builder() + .also { wb -> + wb.security = Config.SecurityConfig.Builder().also { sc -> sc.private_key = secret }.build() + } + .build() + + val line = config.toOneLineString() + + assertTrue(line.contains("private_key=[REDACTED]"), line) + assertFalse(line.contains(secret.hex()), line) + } + + @Test + fun `mqtt credentials are redacted from module config`() { + val config = + ModuleConfig.Builder() + .also { wb -> + wb.mqtt = + ModuleConfig.MQTTConfig.Builder() + .also { mq -> + mq.username = "meshuser" + mq.password = "hunter2" + } + .build() + } + .build() + + val line = config.toOneLineString() + + assertFalse(line.contains("meshuser"), line) + assertFalse(line.contains("hunter2"), line) + } + + @Test + fun `device id is redacted from my node info`() { + val line = MyNodeInfo.Builder().also { wb -> wb.device_id = secret }.build().toOneLineString() + + assertTrue(line.contains("device_id=[REDACTED]"), line) + assertFalse(line.contains(secret.hex()), line) + assertFalse(line.contains('\n'), line) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/WireExtensionsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/WireExtensionsTest.kt index 905956cf74..54e471a3c8 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/WireExtensionsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/WireExtensionsTest.kt @@ -16,10 +16,10 @@ */ package org.meshtastic.core.model.util -import co.touchlab.kermit.LogWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import co.touchlab.kermit.loggerConfigInit +import org.meshtastic.core.testing.CapturingLogWriter import org.meshtastic.proto.Position import kotlin.test.Test import kotlin.test.assertEquals @@ -27,15 +27,7 @@ import kotlin.test.assertNull class WireExtensionsTest { - private class CapturingWriter : LogWriter() { - val severities = mutableListOf() - - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - severities += severity - } - } - - private val writer = CapturingWriter() + private val writer = CapturingLogWriter() private val logger = Logger(loggerConfigInit(writer), tag = "Test") // Garbage bytes that are not a valid encoding of any message: decoding must fail, not merely @@ -47,7 +39,7 @@ class WireExtensionsTest { val result = Position.ADAPTER.decodeOrNull(garbage, logger) assertNull(result) - assertEquals(listOf(Severity.Warn), writer.severities) + assertEquals(listOf(Severity.Warn), writer.entries.map { it.severity }) } @Test @@ -55,7 +47,7 @@ class WireExtensionsTest { val result = Position.ADAPTER.decodeOrNull(bytes = garbage, logger = logger) assertNull(result) - assertEquals(listOf(Severity.Warn), writer.severities) + assertEquals(listOf(Severity.Warn), writer.entries.map { it.severity }) } @Test @@ -63,7 +55,7 @@ class WireExtensionsTest { val result = Position.ADAPTER.decodeOrNull(garbage) assertNull(result) - assertEquals(emptyList(), writer.severities) + assertEquals(emptyList(), writer.entries.map { it.severity }) } @Test @@ -71,6 +63,6 @@ class WireExtensionsTest { val result = Position.ADAPTER.decodeOrNull(bytes = null as ByteArray?, logger = logger) assertNull(result) - assertEquals(emptyList(), writer.severities) + assertEquals(emptyList(), writer.entries.map { it.severity }) } } diff --git a/core/model/src/androidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt b/core/model/src/jvmAndroidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt similarity index 95% rename from core/model/src/androidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt rename to core/model/src/jvmAndroidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt index eeeee0bd59..1bb928c41d 100644 --- a/core/model/src/androidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt +++ b/core/model/src/jvmAndroidMain/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtils.kt @@ -16,8 +16,6 @@ */ package org.meshtastic.core.model.util -import android.os.Build -import androidx.annotation.RequiresApi import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaZoneId import kotlinx.datetime.toLocalDateTime @@ -33,14 +31,12 @@ import java.util.Locale import kotlin.math.abs /** Generates a POSIX time zone string from a [TimeZone]. */ -@RequiresApi(Build.VERSION_CODES.O) fun TimeZone.toPosixString(): String = this.toJavaZoneId().toPosixString() /** * Generates a POSIX time zone string from a [ZoneId]. Uses the specification found * [here](https://www.postgresql.org/docs/current/datetime-posix-timezone-specs.html). */ -@RequiresApi(Build.VERSION_CODES.O) @Suppress("ReturnCount", "MagicNumber") fun ZoneId.toPosixString(): String { val rules = this.rules @@ -78,7 +74,6 @@ fun ZoneId.toPosixString(): String { } /** Formats the time zone short name for a [ZonedDateTime]. */ -@RequiresApi(Build.VERSION_CODES.O) internal fun ZonedDateTime.timeZoneShortName(): String { val formatter = DateTimeFormatter.ofPattern("zzz", Locale.ENGLISH) val shortName = format(formatter) @@ -89,7 +84,6 @@ internal fun ZonedDateTime.timeZoneShortName(): String { private fun formatAbbreviation(abbrev: String): String = if (abbrev.all { it.isLetter() }) abbrev else "<$abbrev>" /** Gets the abbreviation for a given zone and transition rule. */ -@RequiresApi(Build.VERSION_CODES.O) internal fun getTransitionAbbreviation(zone: ZoneId, rule: ZoneOffsetTransitionRule): String { val year = nowInstant.toLocalDateTime(systemTimeZone).year val transition = rule.createTransition(year) @@ -97,7 +91,6 @@ internal fun getTransitionAbbreviation(zone: ZoneId, rule: ZoneOffsetTransitionR } /** Formats a [ZoneOffset] for use in a POSIX string. */ -@RequiresApi(Build.VERSION_CODES.O) @Suppress("MagicNumber") internal fun formatPosixOffset(offset: ZoneOffset): String { val offsetSeconds = -offset.totalSeconds @@ -119,7 +112,6 @@ internal fun formatPosixOffset(offset: ZoneOffset): String { } /** Formats a [ZoneOffsetTransitionRule] for use in a POSIX string. */ -@RequiresApi(Build.VERSION_CODES.O) @Suppress("MagicNumber") internal fun formatTransitionRule(rule: ZoneOffsetTransitionRule): String { val month = rule.month.value diff --git a/core/model/src/jvmTest/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtilsTest.kt b/core/model/src/jvmTest/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtilsTest.kt new file mode 100644 index 0000000000..ba7685669a --- /dev/null +++ b/core/model/src/jvmTest/kotlin/org/meshtastic/core/model/util/PosixTimeZoneUtilsTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model.util + +import kotlinx.datetime.TimeZone +import java.time.ZoneId +import kotlin.test.Test +import kotlin.test.assertEquals + +class PosixTimeZoneUtilsTest { + + private fun posix(zone: String) = ZoneId.of(zone).toPosixString() + + @Test + fun `a zone with daylight saving time lists both transitions`() { + assertEquals("EST5EDT,M3.2.0,M11.1.0", posix("America/New_York")) + } + + @Test + fun `a transition away from 2am carries its wall clock time`() { + assertEquals("GMT0BST,M3.5.0/1,M10.5.0", posix("Europe/London")) + assertEquals("CET-1CEST,M3.5.0,M10.5.0/3", posix("Europe/Berlin")) + } + + @Test + fun `a southern hemisphere zone starts daylight time in its spring month`() { + assertEquals("AEST-10AEDT,M10.1.0,M4.1.0/3", posix("Australia/Sydney")) + } + + @Test + fun `a fixed offset zone has no transitions`() { + assertEquals("IST-5:30", posix("Asia/Kolkata")) + assertEquals("UTC0", posix("UTC")) + } + + @Test + fun `a half hour standard offset keeps its minutes`() { + assertEquals("NST3:30NDT,M3.2.0,M11.1.0", posix("America/St_Johns")) + } + + @Test + fun `a daylight shift other than one hour names the daylight offset`() { + // The JDK has no English abbreviation for Lord Howe, so both names fall back to GMT; the offsets carry it. + assertEquals("GMT-10:30GMT-11,M10.1.0,M4.1.0", posix("Australia/Lord_Howe")) + } + + @Test + fun `the kotlinx time zone overload matches the java one`() { + assertEquals(posix("America/New_York"), TimeZone.of("America/New_York").toPosixString()) + } +} diff --git a/core/navigation/README.md b/core/navigation/README.md index 9b6a0a1d7e..350d37dc31 100644 --- a/core/navigation/README.md +++ b/core/navigation/README.md @@ -41,7 +41,6 @@ graph TB :core:navigation[navigation]:::kmp-library-compose :core:navigation -.-> :core:common :core:navigation -.-> :core:resources - :core:navigation -.-> :core:testing classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index d06be78e49..06bee2de65 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -22,6 +22,7 @@ plugins { } kotlin { + // No withHostTest: commonTest reaches android.net.Uri and Bundle, which the host-test stubs do not implement. sourceSets { commonMain.dependencies { implementation(projects.core.common) @@ -33,7 +34,5 @@ kotlin { implementation(libs.androidx.navigation3.runtime) implementation(libs.kermit) } - - commonTest.dependencies { implementation(projects.core.testing) } } } diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt index ac2286a43a..dc8f5ce678 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt @@ -21,7 +21,6 @@ import androidx.savedstate.serialization.SavedStateConfiguration import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclassesOfSealed /** * Shared polymorphic serialization configuration for Navigation 3 saved-state support. Uses sealed interface diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/RumViewName.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/RumViewName.kt index 4ac78333bc..51391fb559 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/RumViewName.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/RumViewName.kt @@ -19,11 +19,19 @@ package org.meshtastic.core.navigation import androidx.navigation3.runtime.NavKey /** - * Derives the analytics view name for a navigation destination. - * - * The name is the route's fully-qualified class name (e.g. `org.meshtastic.core.navigation.NodesRoute.Nodes`), matching - * the convention historically recorded by Datadog RUM before the Navigation 3 migration, so new per-screen data lines - * up with existing dashboards. Falls back to the simple name (and finally `toString()`) on the rare platform where - * [kotlin.reflect.KClass.qualifiedName] is unavailable. + * Derives the analytics view name for a navigation destination: the route's class name without its package, keeping the + * enclosing route interface so leaf names stay unique (e.g. `NodesRoute.Nodes`, `SettingsRoute.Bluetooth`). */ -fun NavKey.rumViewName(): String = this::class.qualifiedName ?: this::class.simpleName ?: toString() +fun NavKey.rumViewName(): String = rumViewName(this::class.qualifiedName ?: this::class.simpleName ?: toString()) + +/** + * Strips the package from [className], treating leading lowercase segments as the package. Minified builds can report + * the JVM binary name (`NodesRoute$Nodes`), so `$` is normalised to `.` to give every build type the same name. + */ +internal fun rumViewName(className: String): String = className + .split('.', '$') + .dropWhile { it.firstOrNull()?.isLowerCase() == true } + .joinToString(".") + .ifEmpty { + className + } diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/RumViewNameTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/RumViewNameTest.kt index 60f2d72a26..a4537f12c0 100644 --- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/RumViewNameTest.kt +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/RumViewNameTest.kt @@ -16,25 +16,40 @@ */ package org.meshtastic.core.navigation +import androidx.navigation3.runtime.NavKey import kotlin.test.Test import kotlin.test.assertEquals -/** - * Guards the RUM view-name convention consumed by the analytics layer. View names must be the route's fully-qualified - * class name so per-screen RUM data lines up with historical Datadog dashboards. A rename of a route interface or the - * package would break cross-platform data continuity, so this test pins the format. - */ +/** Pins the RUM view-name format that Datadog dashboards and monitors filter `@view.name` on. */ class RumViewNameTest { @Test - fun `rumViewName is the fully qualified route name for data objects`() { - assertEquals("org.meshtastic.core.navigation.NodesRoute.Nodes", NodesRoute.Nodes.rumViewName()) + fun `rumViewName drops the package and keeps the enclosing route`() { + assertEquals("NodesRoute.Nodes", NodesRoute.Nodes.rumViewName()) + assertEquals("SettingsRoute.Bluetooth", SettingsRoute.Bluetooth.rumViewName()) } @Test fun `rumViewName is stable across argument values for data classes`() { - val expected = "org.meshtastic.core.navigation.NodeDetailRoute.DeviceMetrics" + val expected = "NodeDetailRoute.DeviceMetrics" assertEquals(expected, NodeDetailRoute.DeviceMetrics(destNum = 1).rumViewName()) assertEquals(expected, NodeDetailRoute.DeviceMetrics(destNum = 2).rumViewName()) } + + @Test + fun `rumViewName of a top level key is its simple name`() { + assertEquals("TopLevelKey", TopLevelKey.rumViewName()) + } + + @Test + fun `binary names from minified builds match the qualified form`() { + assertEquals("NodesRoute.Nodes", rumViewName("org.meshtastic.core.navigation.NodesRoute\$Nodes")) + } + + @Test + fun `a name with no uppercase segment is kept whole`() { + assertEquals("a.b", rumViewName("a.b")) + } } + +private data object TopLevelKey : NavKey diff --git a/core/network/detekt-baseline.xml b/core/network/detekt-baseline.xml index c373eea433..d05ffe79cb 100644 --- a/core/network/detekt-baseline.xml +++ b/core/network/detekt-baseline.xml @@ -1,5 +1,23 @@ - + + NoNameShadowing:HeartbeatSender.kt:HeartbeatSender$wb + NoNameShadowing:MockRadioTransport.kt:MockRadioTransport$wb + NoNameShadowing:TcpTransport.kt:TcpTransport$wb + UnusedPrivateProperty:AndroidNetworkMonitor.kt:AndroidNetworkMonitor$private val connectivityManager: ConnectivityManager + UnusedPrivateProperty:AndroidRadioTransportFactory.kt:AndroidRadioTransportFactory$private val buildConfigProvider: BuildConfigProvider + UnusedPrivateProperty:AndroidServiceDiscovery.kt:AndroidServiceDiscovery$private val nsdManager: NsdManager + UnusedPrivateProperty:BleRadioTransport.kt:BleRadioTransport$private val connectionFactory: BleConnectionFactory + UnusedPrivateProperty:BleRadioTransport.kt:BleRadioTransport$private val scanner: BleScanner + UnusedPrivateProperty:BleRadioTransport.kt:BleRadioTransport$private val scope: CoroutineScope + UnusedPrivateProperty:JvmServiceDiscovery.kt:JvmServiceDiscovery$private val dispatchers: CoroutineDispatchers + UnusedPrivateProperty:MockRadioTransport.kt:MockRadioTransport$private val scope: CoroutineScope + UnusedPrivateProperty:ReplayRadioTransport.kt:ReplayRadioTransport$private val scope: CoroutineScope + UnusedPrivateProperty:UsbRepository.kt:UsbRepository$private val usbSerialProberLazy: Lazy<UsbSerialProber> + UseOrEmpty:AndroidServiceDiscovery.kt:AndroidServiceDiscovery$info.host?.hostAddress ?: "" + UseOrEmpty:JvmServiceDiscovery.kt:JvmServiceDiscovery.<no name provided>$info.inet4Addresses.firstOrNull()?.hostAddress ?: info.inet6Addresses.firstOrNull()?.hostAddress ?: "" + UseOrEmpty:MQTTRepositoryImpl.kt:MQTTRepositoryImpl$state.lastError?.message?.let { ": $it" } ?: "" + UseOrEmpty:UsbRepository.kt:UsbRepository$usbManagerLazy.value?.deviceList ?: emptyMap() + diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt index 0996a04726..36615d5ab5 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt @@ -17,7 +17,6 @@ package org.meshtastic.core.network.radio import android.content.Context -import android.hardware.usb.UsbManager import android.provider.Settings import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.MutableStateFlow @@ -46,7 +45,6 @@ class AndroidRadioTransportFactory( private val context: Context, private val buildConfigProvider: BuildConfigProvider, private val usbRepository: UsbRepository, - private val usbManager: UsbManager, hiddenFeaturesUnlock: HiddenFeaturesUnlock, scanner: BleScanner, bluetoothRepository: BluetoothRepository, @@ -85,27 +83,11 @@ class AndroidRadioTransportFactory( runCatching { context.assets.open(REPLAY_ASSET_NAME).use { it.read() != -1 } }.getOrDefault(false) } - override fun isPlatformAddressValid(address: String): Boolean { - val interfaceId = address.firstOrNull()?.let { InterfaceId.forIdChar(it) } ?: return false - val rest = address.substring(1) - return when (interfaceId) { - InterfaceId.MOCK, - InterfaceId.NOP, - InterfaceId.REPLAY, - InterfaceId.TCP, - -> true + override val isSerialSupported: Boolean = usbRepository.isSupported - InterfaceId.SERIAL -> { - val deviceMap = usbRepository.serialDevices.value - // Older installs may still hold the former path-based USB address. When exactly one serial device is - // present, retain the historical self-healing fallback instead of rejecting an otherwise usable radio. - val driver = resolveSerialDevice(deviceMap, rest) - driver != null && usbManager.hasPermission(driver.device) - } - - InterfaceId.BLUETOOTH -> true // Handled by base class - } - } + // Presence and USB permission are left to SerialRadioTransport, which reports a denied permission to the user. + override fun isPlatformAddressValid(address: String): Boolean = + address.firstOrNull()?.let { InterfaceId.forIdChar(it) } == InterfaceId.NOP override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport { val interfaceId = address.firstOrNull()?.let { InterfaceId.forIdChar(it) } diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt index e89abbcb35..768501619e 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt @@ -76,7 +76,7 @@ internal class SerialConnectionImpl( override fun connect() { // We shouldn't be able to get this far without a USB subsystem so explode if that isn't true - val usbManager = usbManagerLazy.value!! + val usbManager = checkNotNull(usbManagerLazy.value) { "No USB subsystem" } val usbDeviceConnection = usbManager.openDevice(device.device) if (usbDeviceConnection == null) { diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt index 5b6ef64617..b913546920 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt @@ -25,6 +25,7 @@ import co.touchlab.kermit.Logger import com.hoho.android.usbserial.driver.UsbSerialDriver import com.hoho.android.usbserial.driver.UsbSerialPort import com.hoho.android.usbserial.driver.UsbSerialProber +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -38,6 +39,7 @@ import kotlinx.coroutines.withContext import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.di.PROCESS_LIFECYCLE +import org.meshtastic.core.common.hasUsbHost import org.meshtastic.core.common.util.ignoreException import org.meshtastic.core.common.util.registerReceiverCompat import org.meshtastic.core.di.CoroutineDispatchers @@ -58,6 +60,9 @@ class UsbRepository( private val usbManagerLazy: Lazy, private val usbSerialProberLazy: Lazy, ) { + /** False when the device cannot act as a USB host, so no USB serial transport can ever work (e.g. Android XR). */ + val isSupported: Boolean = application.hasUsbHost() + private val _serialDevices = MutableStateFlow(emptyMap()) val serialDevices = @@ -127,6 +132,8 @@ class UsbRepository( port.rts = true delay(holdMillis) true + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.w(e) { "DTR poke failed for ${driver.device.usbSerialStableKey()}" } false diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BaseRadioTransportFactory.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BaseRadioTransportFactory.kt index 5a99d69932..e16401f2d5 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BaseRadioTransportFactory.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BaseRadioTransportFactory.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.network.radio +import co.touchlab.kermit.Logger import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.ble.BluetoothRepository @@ -36,14 +37,22 @@ abstract class BaseRadioTransportFactory( protected val dispatchers: CoroutineDispatchers, ) : RadioTransportFactory { + init { + if (!bluetoothRepository.isSupported) Logger.w { "No Bluetooth LE on this hardware; BLE addresses are refused" } + } + override fun isAddressValid(address: String?): Boolean { val spec = address?.firstOrNull() ?: return false return when (spec) { - InterfaceId.TCP.id, - InterfaceId.SERIAL.id, + InterfaceId.TCP.id -> true + + // A saved serial address restored onto hardware with no USB host is kept but never armed. + InterfaceId.SERIAL.id -> isSerialSupported + + // A saved BLE address restored onto hardware with no Bluetooth LE is kept but never armed. InterfaceId.BLUETOOTH.id, '!', - -> true + -> bluetoothRepository.isSupported // Virtual transports stay inadmissible until deliberately enabled: `connections?address=m` is reachable // from any web page through the verified meshtastic.org app link, so a drive-by deep link must not be able @@ -56,6 +65,9 @@ abstract class BaseRadioTransportFactory( } } + /** Whether this hardware can host a USB serial radio. */ + protected open val isSerialSupported: Boolean = true + protected open fun isPlatformAddressValid(address: String): Boolean = false override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest" diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt index 688b0b823b..12e3811adb 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt @@ -161,7 +161,7 @@ class BleRadioTransport( private val cleanupScope: CoroutineScope = CoroutineScope(SupervisorJob() + scope.coroutineContext.minusKey(Job)) private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Logger.w(throwable) { "[$address] Uncaught exception in connectionScope" } + Logger.w(throwable) { "[${address.anonymize()}] Uncaught exception in connectionScope" } if (throwable !is CancellationException) { val session = activeSession.value if (session != null) { @@ -173,7 +173,9 @@ class BleRadioTransport( if (activeSession.value == null) { disconnectGatt("exception handler") } else { - Logger.d { "[$address] Skipping exception-handler GATT release; a new session is active" } + Logger.d { + "[${address.anonymize()}] Skipping exception-handler GATT release; a new session is active" + } } } } @@ -261,41 +263,42 @@ class BleRadioTransport( // --- Connection & Discovery Logic --- private fun connect() { - connectionJob = - connectionScope.launch { - reconnectPolicy.execute( - attempt = { - try { - attemptConnection() - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - val failureTime = (nowMillis - connectionStartTime).milliseconds - Logger.w(e) { "[$address] Failed to connect after $failureTime" } - BleReconnectPolicy.Outcome.Failed(e) - } - }, - onTransientDisconnect = { error -> - // Guard: if handleFailure already emitted the disconnect callback for this - // session (sessionFailed CAS won), don't emit a duplicate from the policy. - // Silent recovery: no errorMessage — the reconnect loop is still retrying, so - // a modal dialog would just confuse the user. The warning log is the - // observability surface for this transient event. - if (!sessionFailed.value) { - error?.let { - Logger.w(it) { "[$address] BLE reconnect attempt failed; continuing automatic retry" } + connectionJob = connectionScope.launch { + reconnectPolicy.execute( + attempt = { + try { + attemptConnection() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + val failureTime = (nowMillis - connectionStartTime).milliseconds + Logger.w(e) { "[${address.anonymize()}] Failed to connect after $failureTime" } + BleReconnectPolicy.Outcome.Failed(e) + } + }, + onTransientDisconnect = { error -> + // Guard: if handleFailure already emitted the disconnect callback for this + // session (sessionFailed CAS won), don't emit a duplicate from the policy. + // Silent recovery: no errorMessage — the reconnect loop is still retrying, so + // a modal dialog would just confuse the user. The warning log is the + // observability surface for this transient event. + if (!sessionFailed.value) { + error?.let { + Logger.w(it) { + "[${address.anonymize()}] BLE reconnect attempt failed; continuing automatic retry" } - callback.onDisconnect(isPermanent = false) } - }, - onPermanentDisconnect = { error -> - if (!sessionFailed.value) { - val msg = error?.toDisconnectReason()?.second ?: "Device unreachable" - callback.onDisconnect(isPermanent = true, errorMessage = msg) - } - }, - ) - } + callback.onDisconnect(isPermanent = false) + } + }, + onPermanentDisconnect = { error -> + if (!sessionFailed.value) { + val msg = error?.toDisconnectReason()?.second ?: "Device unreachable" + callback.onDisconnect(isPermanent = true, errorMessage = msg) + } + }, + ) + } } /** @@ -307,7 +310,7 @@ class BleRadioTransport( @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount") private suspend fun attemptConnection(): BleReconnectPolicy.Outcome { connectionStartTime = nowMillis - Logger.i { "[$address] BLE connection attempt started" } + Logger.i { "[${address.anonymize()}] BLE connection attempt started" } awaitPendingSessionCleanup() sessionFailed.value = false @@ -354,7 +357,7 @@ class BleRadioTransport( val state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT) if (state !is BleConnectionState.Connected) { - throw RadioNotConnectedException("Failed to connect to device at address $address") + throw RadioNotConnectedException("Failed to connect to device at address ${address.anonymize()}") } // GATT cache invalidation has two triggers, both repaired the same way — refresh the platform's cached @@ -389,7 +392,9 @@ class BleRadioTransport( // If a fatal session failure (fromRadio/logRadio error) forced disconnect during setup, // skip the Connected gate — return a retryable failure so BleReconnectPolicy handles it. session.failureCause.value?.let { failure -> - Logger.w(failure) { "[$address] Session failed during profile setup — returning failed outcome" } + Logger.w(failure) { + "[${address.anonymize()}] Session failed during profile setup; returning failed outcome" + } return BleReconnectPolicy.Outcome.Failed(failure) } @@ -412,7 +417,9 @@ class BleRadioTransport( } if (connectedReached == null) { val failure = session.failureCause.value ?: RuntimeException("Timed out waiting for Connected state gate") - Logger.w(failure) { "[$address] Session failed before Connected gate — returning failed outcome" } + Logger.w(failure) { + "[${address.anonymize()}] Session failed before Connected gate; returning failed outcome" + } // Force cleanup only for this exact profile generation. If another path already retired it, await that // generation's cleanup instead of issuing a second disconnect that could race later lifecycle work. isFullyConnected = false @@ -443,14 +450,16 @@ class BleRadioTransport( onDisconnected(session) } - Logger.i { "[$address] BLE connection dropped (reason: $disconnectReason), preparing to reconnect" } + Logger.i { + "[${address.anonymize()}] BLE connection dropped (reason: $disconnectReason), preparing to reconnect" + } // Internal session failures (write/read exceptions that triggered handleFailure → // disconnect) must NOT be treated as intentional/user disconnects — the reconnect policy // needs to escalate backoff for these. val internalFailure = session.failureCause.value if (internalFailure != null) { - Logger.w(internalFailure) { "[$address] Session forced disconnect due to internal failure" } + Logger.w(internalFailure) { "[${address.anonymize()}] Session forced disconnect due to internal failure" } } val wasIntentional = if (internalFailure != null) { @@ -469,7 +478,7 @@ class BleRadioTransport( if (!wasStable && !wasIntentional) { Logger.w { - "[$address] Connection lasted only $connectionUptime " + + "[${address.anonymize()}] Connection lasted only $connectionUptime " + "(< ${reconnectPolicy.minStableConnection}) — treating as unstable" } } @@ -520,10 +529,10 @@ class BleRadioTransport( // Bond before connecting: firmware may require an encrypted link, and without a bond Android fails with // status 5 or 133. Non-Android targets use repository-specific no-op behavior. - Logger.i { "[$address] Device not bonded, initiating bonding" } + Logger.i { "[${address.anonymize()}] Device not bonded, initiating bonding" } try { bluetoothRepository.bond(device) - Logger.i { "[$address] Bonding successful" } + Logger.i { "[${address.anonymize()}] Bonding successful" } } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -531,9 +540,12 @@ class BleRadioTransport( // setup. If the device is still not bonded, continuing would fail later with a cryptic status (5/133), so // stop now and let BleReconnectPolicy own the retry/backoff. if (bluetoothRepository.isBonded(address)) { - Logger.w(e) { "[$address] Bonding reported failure but device is bonded; continuing" } + Logger.w(e) { "[${address.anonymize()}] Bonding reported failure but device is bonded; continuing" } } else { - Logger.w(e) { "[$address] Bonding failed and device is still not bonded; stopping connection attempt" } + Logger.w(e) { + "[${address.anonymize()}] Bonding failed and device is still not bonded; " + + "stopping connection attempt" + } throw RadioNotConnectedException("Bonding failed and device is still not bonded", e) } } @@ -542,7 +554,7 @@ class BleRadioTransport( private suspend fun onConnected() { try { bleConnection.deviceFlow.first()?.let { device -> - val rssi = retryBleOperation(tag = address) { device.readRssi() } + val rssi = retryBleOperation(tag = address.anonymize()) { device.readRssi() } Logger.d { "[${address.anonymize()}] Connection confirmed. " + "Initial RSSI: ${rssi?.let { "$it dBm" } ?: "unknown"}" @@ -551,7 +563,7 @@ class BleRadioTransport( } catch (e: CancellationException) { throw e } catch (e: Exception) { - Logger.w(e) { "[$address] Failed to read initial connection RSSI" } + Logger.w(e) { "[${address.anonymize()}] Failed to read initial connection RSSI" } } } @@ -560,11 +572,12 @@ class BleRadioTransport( scheduleSessionCleanup(retired, disconnectGatt = false, phase = "remote disconnect") // Atomic first-writer-wins: if another failure already claimed this session's callback, skip the duplicate. val firstWriter = sessionFailed.compareAndSet(expect = false, update = true) - Logger.i { "[$address] BLE disconnected - ${formatSessionStats()}" } + Logger.i { "[${address.anonymize()}] BLE disconnected - ${formatSessionStats()}" } if (firstWriter) callback.onDisconnect(isPermanent = false) } - @Suppress("LongMethod", "ThrowsCount") + // Cancellation runs GATT cleanup under NonCancellable, then is rethrown. + @Suppress("LongMethod", "ThrowsCount", "SuspendFunSwallowedCancellation") private suspend fun discoverServicesAndSetupCharacteristics(): BleSession { var setupSession: BleSession? = null try { @@ -578,27 +591,27 @@ class BleRadioTransport( radioService.fromRadio .onEach { packet -> - Logger.v { "[$address] Received packet fromRadio (${packet.size} bytes)" } + Logger.v { "[${address.anonymize()}] Received packet fromRadio (${packet.size} bytes)" } dispatchPacket(packet, session) } .catch { e -> - Logger.w(e) { "[$address] Error in fromRadio flow" } + Logger.w(e) { "[${address.anonymize()}] Error in fromRadio flow" } handleFailure(e, session) } .launchIn(this) radioService.logRadio .onEach { packet -> - Logger.v { "[$address] Received packet logRadio (${packet.size} bytes)" } + Logger.v { "[${address.anonymize()}] Received packet logRadio (${packet.size} bytes)" } dispatchPacket(packet, session) } .catch { e -> - Logger.w(e) { "[$address] Error in logRadio flow" } + Logger.w(e) { "[${address.anonymize()}] Error in logRadio flow" } handleFailure(e, session) } .launchIn(this) - Logger.i { "[$address] Profile service active and characteristics subscribed" } + Logger.i { "[${address.anonymize()}] Profile service active and characteristics subscribed" } // Wait for FROMNUM CCCD write before triggering the Meshtastic handshake. // Bounded: if fromRadio fails before subscriptionReady completes, handleFailure @@ -617,14 +630,17 @@ class BleRadioTransport( ?: RuntimeException("Timed out waiting for FROMNUM subscription readiness") Logger.w(cause) { val reason = if (!subscriptionReady) "timed out" else "failed" - "[$address] Subscription wait $reason — aborting setup" + "[${address.anonymize()}] Subscription wait $reason; aborting setup" } throw cause } // Log negotiated MTU for diagnostics val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE) - Logger.i { "[$address] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): $maxLen bytes" } + Logger.i { + "[${address.anonymize()}] BLE Radio Session Ready. " + + "Max write length (WITHOUT_RESPONSE): $maxLen bytes" + } requestHighPriorityAndScheduleDowngrade() @@ -656,7 +672,9 @@ class BleRadioTransport( false } if (!published) { - Logger.w { "[$address] Session failed or transport closed during setup — skipping onConnect" } + Logger.w { + "[${address.anonymize()}] Session failed or transport closed during setup; skipping onConnect" + } } } return checkNotNull(setupSession) { "BLE profile setup completed without publishing a session" } @@ -666,7 +684,7 @@ class BleRadioTransport( withContext(NonCancellable) { cleanupProfileSetupFailure("cancellation cleanup", setupSession) } throw e } catch (e: Exception) { - Logger.w(e) { "[$address] Profile service discovery or operation failed" } + Logger.w(e) { "[${address.anonymize()}] Profile service discovery or operation failed" } // Retire any partially-published profile so the next attempt starts clean. Without this, a failure after // profile publication but before callback.onConnect() could leave a stale generation behind. withContext(NonCancellable) { cleanupProfileSetupFailure("profile error cleanup", setupSession) } @@ -677,7 +695,7 @@ class BleRadioTransport( private suspend fun cleanupProfileSetupFailure(phase: String, expectedSession: BleSession?) { val currentSession = activeSession.value if (expectedSession != null && currentSession != null && currentSession !== expectedSession) { - Logger.w { "[$address] Ignoring $phase from an unpublished BLE profile generation" } + Logger.w { "[${address.anonymize()}] Ignoring $phase from an unpublished BLE profile generation" } return } @@ -706,14 +724,14 @@ class BleRadioTransport( */ private suspend fun CoroutineScope.requestHighPriorityAndScheduleDowngrade() { if (bleConnection.requestHighConnectionPriority()) { - Logger.d { "[$address] Requested high BLE connection priority" } + Logger.d { "[${address.anonymize()}] Requested high BLE connection priority" } // Wait for the connection parameter update before starting heavy traffic. delay(1.seconds) } launch { delay(PRIORITY_DOWNGRADE_DELAY) if (bleConnection.requestBalancedConnectionPriority()) { - Logger.d { "[$address] Downgraded to balanced BLE connection priority" } + Logger.d { "[${address.anonymize()}] Downgraded to balanced BLE connection priority" } } } } @@ -774,13 +792,12 @@ class BleRadioTransport( // Admission bounds the number of queued writes. Start the per-write timeout only after this operation reaches // the front of that bounded queue, so normal backlog does not masquerade as a dead BLE link. Four admitted // writes at the worst-case 10s write bound fit inside the 45s lifecycle drain budget. - val completed = - writeMutex.withLock { - withTimeoutOrNull(BLE_WRITE_OPERATION_TIMEOUT) { - writePacket(session, packet) - true - } == true - } + val completed = writeMutex.withLock { + withTimeoutOrNull(BLE_WRITE_OPERATION_TIMEOUT) { + writePacket(session, packet) + true + } == true + } if (!completed && activeSession.value === session) { handleFailure(RadioNotConnectedException("BLE write timed out after $BLE_WRITE_OPERATION_TIMEOUT"), session) } @@ -788,23 +805,26 @@ class BleRadioTransport( private suspend fun writePacket(session: BleSession, packet: ByteArray) { try { - retryBleOperation(tag = address, retryWhile = { activeSession.value === session }) { + retryBleOperation(tag = address.anonymize(), retryWhile = { activeSession.value === session }) { session.profile.sendToRadio(packet) } val sent = packetsSent.incrementAndGet() val txBytes = bytesSent.addAndGet(packet.size.toLong()) - Logger.v { "[$address] Wrote packet #$sent to toRadio (${packet.size} bytes, total TX: $txBytes bytes)" } + Logger.v { + "[${address.anonymize()}] Wrote packet #$sent to toRadio " + + "(${packet.size} bytes, total TX: $txBytes bytes)" + } } catch (e: CancellationException) { throw e } catch (e: Exception) { if (activeSession.value === session) { Logger.w(e) { - "[$address] Failed to write packet to toRadioCharacteristic after " + + "[${address.anonymize()}] Failed to write packet to toRadioCharacteristic after " + "${packetsSent.value} successful writes" } handleFailure(e, session) } else { - Logger.d(e) { "[$address] Stale write failure ignored because the session was replaced" } + Logger.d(e) { "[${address.anonymize()}] Stale write failure ignored because the session was replaced" } } } } @@ -820,21 +840,24 @@ class BleRadioTransport( override suspend fun close() { var completed = false try { - completed = - lifecycle.close { - // Closing the outer gate rejects new sends while allowing writes admitted before close to finish. - // Once those leases drain, cancel reconnect/heartbeat work before retiring the profile and GATT. - connectionScope.cancel() - Logger.i { "[$address] Disconnecting. ${formatSessionStats()}" } - val session = retireActiveSession() - val sessionClosed = session?.lifecycle?.close() ?: true - awaitPendingSessionCleanup() - disconnectGatt("close") - if (!sessionClosed) { - Logger.w { "[$address] BLE profile teardown did not complete within its lifecycle bounds" } + completed = lifecycle.close { + // Closing the outer gate rejects new sends while allowing writes admitted before close to finish. + // Once those leases drain, cancel reconnect/heartbeat work before retiring the profile and GATT. + connectionScope.cancel() + Logger.i { "[${address.anonymize()}] Disconnecting. ${formatSessionStats()}" } + val session = retireActiveSession() + val sessionClosed = session?.lifecycle?.close() ?: true + awaitPendingSessionCleanup() + disconnectGatt("close") + if (!sessionClosed) { + Logger.w { + "[${address.anonymize()}] BLE profile teardown did not complete within its lifecycle bounds" } } - if (!completed) Logger.w { "[$address] BLE teardown did not complete within its lifecycle bounds" } + } + if (!completed) { + Logger.w { "[${address.anonymize()}] BLE teardown did not complete within its lifecycle bounds" } + } } finally { if (!completed) { // The cleanup scope is detached, so a timed-out outer gate needs one final bounded GATT release attempt @@ -852,7 +875,8 @@ class BleRadioTransport( val received = packetsReceived.incrementAndGet() val rxBytes = bytesReceived.addAndGet(packet.size.toLong()) Logger.v { - "[$address] Dispatching packet #$received " + "(${packet.size} bytes, total RX: $rxBytes bytes)" + "[${address.anonymize()}] Dispatching packet #$received " + + "(${packet.size} bytes, total RX: $rxBytes bytes)" } callback.handleFromRadio(packet) true @@ -870,7 +894,7 @@ class BleRadioTransport( recordSessionFailureCause(throwable, expectedSession) val retired = retireActiveSession(expectedSession) if (retired == null) { - Logger.d(throwable) { "[$address] Ignoring failure from a retired BLE profile generation" } + Logger.d(throwable) { "[${address.anonymize()}] Ignoring failure from a retired BLE profile generation" } return } val firstFailure = sessionFailed.compareAndSet(expect = false, update = true) @@ -879,7 +903,7 @@ class BleRadioTransport( val (isPermanent, msg) = throwable.toDisconnectReason() callback.onDisconnect(isPermanent, errorMessage = if (isPermanent) msg else null) } - Logger.w(throwable) { "[$address] Session failure — forcing cleanup for reconnect" } + Logger.w(throwable) { "[${address.anonymize()}] Session failure; forcing cleanup for reconnect" } scheduleSessionCleanup(retired, disconnectGatt = true, phase = "session failure") } @@ -901,7 +925,9 @@ class BleRadioTransport( } else { session.lifecycle.close() } - if (!completed) Logger.w { "[$address] BLE profile cleanup timed out during $phase" } + if (!completed) { + Logger.w { "[${address.anonymize()}] BLE profile cleanup timed out during $phase" } + } } .also { pendingSessionCleanup = it } } @@ -927,7 +953,7 @@ class BleRadioTransport( } catch (e: CancellationException) { throw e } catch (e: Exception) { - Logger.w(e) { "[$address] Failed to disconnect during $phase" } + Logger.w(e) { "[${address.anonymize()}] Failed to disconnect during $phase" } } } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt index 83753a5124..134c768580 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay +import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.handledLaunch @@ -39,7 +40,6 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.FromRadio -import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.Neighbor @@ -54,7 +54,6 @@ import org.meshtastic.proto.ToRadio import org.meshtastic.proto.User import org.meshtastic.proto.Channel as ProtoChannel import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo -import org.meshtastic.proto.Position as ProtoPosition private val defaultLoRaConfig = Config.LoRaConfig.Builder() @@ -99,6 +98,9 @@ class MockRadioTransport( val address: String, ) : RadioTransport { + /** The mesh this transport plays, picked by the address suffix; see [MockScenario.forAddress]. */ + private val scenario = MockScenario.forAddress(address) + /** * Hands out packet ids. * @@ -167,7 +169,7 @@ class MockRadioTransport( } data != null && data.portnum == PortNum.TEXT_MESSAGE_APP -> { - if (packet?.want_ack == true) sendFakeAck(pr) + if (packet.want_ack) sendFakeAck(pr) sendSimulatedReply(packet) } @@ -219,7 +221,7 @@ class MockRadioTransport( .also { wb -> wb.statusmessage = ModuleConfig.StatusMessageConfig.Builder() - .also { wb -> wb.node_status = MY_NODE_STATUS } + .also { wb -> wb.node_status = scenario.nodeStatus } .build() } .build() @@ -248,8 +250,8 @@ class MockRadioTransport( val metadata = DeviceMetadata.Builder() .also { wb -> - wb.firmware_version = FIRMWARE_VERSION - wb.hw_model = HardwareModel.ANDROID_SIM + wb.firmware_version = scenario.firmwareVersion + wb.hw_model = scenario.hwModel } .build() val frames = @@ -259,7 +261,7 @@ class MockRadioTransport( wb.my_info = ProtoMyNodeInfo.Builder() .also { wb -> - wb.my_node_num = MY_NODE + wb.my_node_num = scenario.myNode wb.reboot_count = 3 } .build() @@ -307,9 +309,9 @@ class MockRadioTransport( /** Stage 2: the node database. This is the only window in which the app accepts `node_info`. */ private fun sendNodeInfoStage() { - Logger.d { "Mock transport answering node-info stage with ${SIM_PEERS.size + 1} nodes" } + Logger.d { "Mock transport answering node-info stage with ${scenario.peers.size + 1} nodes" } callback.handleFromRadio(FromRadio.Builder().also { wb -> wb.node_info = localNodeInfo() }.build().encode()) - SIM_PEERS.forEach { peer -> + scenario.peers.forEach { peer -> callback.handleFromRadio( FromRadio.Builder().also { wb -> wb.node_info = peer.toNodeInfo() }.build().encode(), ) @@ -328,24 +330,24 @@ class MockRadioTransport( private fun localNodeInfo() = NodeInfo.Builder() .also { wb -> - wb.num = MY_NODE + wb.num = scenario.myNode wb.last_heard = nowSeconds.toInt() wb.user = User.Builder() .also { wb -> - wb.id = NodeAddress.numToDefaultId(MY_NODE) - wb.long_name = "Demo Handset" - wb.short_name = "DEMO" - wb.hw_model = HardwareModel.ANDROID_SIM + wb.id = NodeAddress.numToDefaultId(scenario.myNode) + wb.long_name = scenario.longName + wb.short_name = scenario.shortName + wb.hw_model = scenario.hwModel wb.role = Config.DeviceConfig.Role.CLIENT } .build() - wb.position = MY_POSITION.toProto() + wb.position = scenario.position.toProto() wb.device_metrics = DeviceMetrics.Builder() .also { wb -> - wb.battery_level = 78 - wb.voltage = 3.98f + wb.battery_level = scenario.myBatteryLevel + wb.voltage = scenario.myVoltage wb.channel_utilization = 8.4f wb.air_util_tx = 1.9f wb.uptime_seconds = 7_240 @@ -370,6 +372,7 @@ class MockRadioTransport( wb.short_name = shortName wb.hw_model = hwModel wb.role = role + publicKey?.let { wb.public_key = it.decodeHex() } } .build() wb.position = SimPosition(latitude, longitude, altitude).toProto() @@ -378,12 +381,15 @@ class MockRadioTransport( .also { wb -> wb.battery_level = batteryLevel wb.voltage = voltage + wb.channel_utilization = channelUtilization + wb.air_util_tx = airUtilTx wb.uptime_seconds = uptimeSeconds } .build() wb.snr = snr wb.hops_away = hops wb.channel = 0 + wb.is_favorite = favorite } .build() @@ -397,35 +403,62 @@ class MockRadioTransport( * points and messages. */ private suspend fun seedTraffic() { - SIM_PEERS.forEach { peer -> - lifecycle.runIfOpen { callback.handleFromRadio(peer.positionPacket(nextPacketId()).encode()) } - delay(SEED_SPACING_MS) - } - - SIM_PEERS.take(TELEMETRY_PEER_COUNT).forEach { peer -> + scenario.peers.forEach { peer -> lifecycle.runIfOpen { - callback.handleFromRadio(peer.deviceTelemetryPacket(nextPacketId(), tick = 0).encode()) + callback.handleFromRadio(peer.positionPacket(nextPacketId(), peer.secondsSinceHeard).encode()) } delay(SEED_SPACING_MS) } - WEATHER_PEER_INDEXES.forEach { index -> - val peer = SIM_PEERS[index] + scenario.peers.take(scenario.telemetryPeerCount).forEach { peer -> lifecycle.runIfOpen { - callback.handleFromRadio(peer.environmentTelemetryPacket(nextPacketId(), tick = 0).encode()) + callback.handleFromRadio( + peer.deviceTelemetryPacket(nextPacketId(), tick = 0, ageSeconds = peer.secondsSinceHeard).encode(), + ) } delay(SEED_SPACING_MS) } - lifecycle.runIfOpen { callback.handleFromRadio(SIM_PEERS[0].neighborInfoPacket(nextPacketId()).encode()) } - delay(SEED_SPACING_MS) - lifecycle.runIfOpen { callback.handleFromRadio(SIM_PEERS[1].nodeStatusPacket(nextPacketId()).encode()) } - delay(SEED_SPACING_MS) + scenario.peers.forEach { peer -> + val environment = peer.environment ?: return@forEach + lifecycle.runIfOpen { + callback.handleFromRadio( + peer + .environmentTelemetryPacket(nextPacketId(), environment, tick = 0, peer.secondsSinceHeard) + .encode(), + ) + } + delay(SEED_SPACING_MS) + } + lifecycle.runIfOpen { + callback.handleFromRadio( + scenario.peers[0].let { it.neighborInfoPacket(nextPacketId(), it.secondsSinceHeard) }.encode(), + ) + } + delay(SEED_SPACING_MS) + scenario.peers.forEach { peer -> + val status = peer.status ?: return@forEach + lifecycle.runIfOpen { + callback.handleFromRadio(peer.nodeStatusPacket(nextPacketId(), status, peer.secondsSinceHeard).encode()) + } + delay(SEED_SPACING_MS) + } + + seedConversations() + + if (scenario.liveTelemetry) streamLiveTelemetry() + } + + /** + * Seeded last: the app takes a node's last-heard time from the latest packet it processes, so a peer that spoke + * reads as heard when it last spoke. + */ + private suspend fun seedConversations() { // Each message is stamped progressively closer to now, so the thread reads as a conversation that unfolded over // the last while rather than a block of messages that all arrived in the same second. - CHANNEL_CONVERSATION.forEachIndexed { index, (peerIndex, text) -> - val peer = SIM_PEERS[peerIndex] + scenario.channelConversation.forEachIndexed { index, (peerIndex, text) -> + val peer = scenario.peers[peerIndex] lifecycle.runIfOpen { callback.handleFromRadio( peer @@ -433,7 +466,7 @@ class MockRadioTransport( id = nextPacketId(), to = BROADCAST_ADDR, text = text, - ageSeconds = messageAgeSeconds(CHANNEL_CONVERSATION.size, index), + ageSeconds = messageAgeSeconds(scenario.channelConversation.size, index), ) .encode(), ) @@ -441,22 +474,21 @@ class MockRadioTransport( delay(SEED_SPACING_MS) } - DIRECT_CONVERSATION.forEachIndexed { index, text -> + scenario.directConversation.forEachIndexed { index, text -> lifecycle.runIfOpen { callback.handleFromRadio( - SIM_PEERS[DIRECT_PEER_INDEX].textPacket( - id = nextPacketId(), - to = MY_NODE, - text = text, - ageSeconds = messageAgeSeconds(DIRECT_CONVERSATION.size, index), - ) + scenario.peers[scenario.directPeerIndex] + .textPacket( + id = nextPacketId(), + to = scenario.myNode, + text = text, + ageSeconds = messageAgeSeconds(scenario.directConversation.size, index), + ) .encode(), ) } delay(SEED_SPACING_MS) } - - streamLiveTelemetry() } /** How long ago the message at [index] of a [count]-message seeded thread was "received". Oldest first. */ @@ -470,12 +502,15 @@ class MockRadioTransport( var tick = 1 while (true) { delay(LIVE_TICK_MS) - val peer = SIM_PEERS[tick % TELEMETRY_PEER_COUNT] + val peer = scenario.peers[tick % scenario.telemetryPeerCount] lifecycle.runIfOpen { callback.handleFromRadio(peer.deviceTelemetryPacket(nextPacketId(), tick).encode()) } - if (tick % WEATHER_TICK_INTERVAL == 0) { - val weatherPeer = SIM_PEERS[WEATHER_PEER_INDEXES.first()] + val weatherPeer = scenario.peers.firstOrNull { it.environment != null } + val environment = weatherPeer?.environment + if (tick % WEATHER_TICK_INTERVAL == 0 && environment != null) { lifecycle.runIfOpen { - callback.handleFromRadio(weatherPeer.environmentTelemetryPacket(nextPacketId(), tick).encode()) + callback.handleFromRadio( + weatherPeer.environmentTelemetryPacket(nextPacketId(), environment, tick).encode(), + ) } } tick++ @@ -491,11 +526,11 @@ class MockRadioTransport( val isBroadcast = packet.to == BROADCAST_ADDR val responder = if (isBroadcast) { - SIM_PEERS[DIRECT_PEER_INDEX] + scenario.peers[scenario.directPeerIndex] } else { - SIM_PEERS.firstOrNull { it.num == packet.to } ?: return + scenario.peers.firstOrNull { it.num == packet.to } ?: return } - val replyTo = if (isBroadcast) BROADCAST_ADDR else MY_NODE + val replyTo = if (isBroadcast) BROADCAST_ADDR else scenario.myNode transportScope.handledLaunch { delay(REPLY_DELAY_MS) @@ -551,13 +586,13 @@ class MockRadioTransport( } .build() - private fun SimPeer.positionPacket(id: Int) = FromRadio.Builder() + private fun SimPeer.positionPacket(id: Int, ageSeconds: Int = 0) = FromRadio.Builder() .also { wb -> wb.packet = packet( id = id, to = BROADCAST_ADDR, - ageSeconds = 0, + ageSeconds = ageSeconds, data = Data.Builder() .also { wb -> @@ -577,16 +612,24 @@ class MockRadioTransport( * takes the voltage through 0V inside a couple of hours and the battery and telemetry views then render a cell that * cannot physically exist. */ - private fun SimPeer.deviceTelemetryPacket(id: Int, tick: Int): FromRadio { - val driftedBattery = (batteryLevel - tick).coerceIn(MIN_BATTERY_PERCENT, MAX_BATTERY_PERCENT) - val driftedVoltage = (voltage - tick * VOLTAGE_DRIFT_PER_TICK).coerceAtLeast(MIN_CELL_VOLTAGE) + private fun SimPeer.deviceTelemetryPacket(id: Int, tick: Int, ageSeconds: Int = 0): FromRadio { + // Above 100 is no battery or charging, neither of which drains. + val powered = batteryLevel > MAX_BATTERY_PERCENT + val driftedBattery = + if (powered) batteryLevel else (batteryLevel - tick).coerceIn(MIN_BATTERY_PERCENT, MAX_BATTERY_PERCENT) + val driftedVoltage = + if (powered) { + voltage + } else { + voltage?.let { (it - tick * VOLTAGE_DRIFT_PER_TICK).coerceAtLeast(MIN_CELL_VOLTAGE) } + } return FromRadio.Builder() .also { wb -> wb.packet = packet( id = id, to = BROADCAST_ADDR, - ageSeconds = 0, + ageSeconds = ageSeconds, data = Data.Builder() .also { wb -> @@ -599,8 +642,9 @@ class MockRadioTransport( .also { wb -> wb.battery_level = driftedBattery wb.voltage = driftedVoltage - wb.channel_utilization = 6f + (tick % 5) * 1.5f - wb.air_util_tx = 1.2f + (tick % 4) * 0.4f + wb.channel_utilization = + (channelUtilization ?: 6f) + (tick % 5) * 1.5f + wb.air_util_tx = (airUtilTx ?: 1.2f) + (tick % 4) * 0.4f wb.uptime_seconds = uptimeSeconds + tick * (LIVE_TICK_MS / 1000).toInt() } @@ -616,13 +660,18 @@ class MockRadioTransport( .build() } - private fun SimPeer.environmentTelemetryPacket(id: Int, tick: Int) = FromRadio.Builder() + private fun SimPeer.environmentTelemetryPacket( + id: Int, + environment: SimEnvironment, + tick: Int, + ageSeconds: Int = 0, + ) = FromRadio.Builder() .also { wb -> wb.packet = packet( id = id, to = BROADCAST_ADDR, - ageSeconds = 0, + ageSeconds = ageSeconds, data = Data.Builder() .also { wb -> @@ -636,9 +685,14 @@ class MockRadioTransport( // Temperature AND humidity must both be present or the // Environment tab // stays empty. - wb.temperature = 18.5f + (tick % 7) * 0.4f - wb.relative_humidity = 47f + (tick % 5) * 1.5f - wb.barometric_pressure = 1013.2f + (tick % 3) * 0.3f + wb.temperature = environment.temperature + (tick % 7) * 0.4f + wb.relative_humidity = + environment.relativeHumidity + (tick % 5) * 1.5f + wb.barometric_pressure = + environment.barometricPressure + (tick % 3) * 0.3f + wb.iaq = environment.iaq + wb.voltage = environment.voltage + wb.current = environment.current } .build() } @@ -651,13 +705,13 @@ class MockRadioTransport( } .build() - private fun SimPeer.neighborInfoPacket(id: Int) = FromRadio.Builder() + private fun SimPeer.neighborInfoPacket(id: Int, ageSeconds: Int = 0) = FromRadio.Builder() .also { wb -> wb.packet = packet( id = id, to = BROADCAST_ADDR, - ageSeconds = 0, + ageSeconds = ageSeconds, data = Data.Builder() .also { wb -> @@ -669,7 +723,7 @@ class MockRadioTransport( wb.last_sent_by_id = num wb.node_broadcast_interval_secs = 900 wb.neighbors = - SIM_PEERS.drop(1).take(3).map { neighbor -> + scenario.peers.drop(1).take(3).map { neighbor -> Neighbor.Builder() .also { wb -> wb.node_id = neighbor.num @@ -689,20 +743,20 @@ class MockRadioTransport( } .build() - private fun SimPeer.nodeStatusPacket(id: Int) = FromRadio.Builder() + private fun SimPeer.nodeStatusPacket(id: Int, status: String, ageSeconds: Int = 0) = FromRadio.Builder() .also { wb -> wb.packet = packet( id = id, to = BROADCAST_ADDR, - ageSeconds = 0, + ageSeconds = ageSeconds, data = Data.Builder() .also { wb -> wb.portnum = PortNum.NODE_STATUS_APP wb.payload = StatusMessage.Builder() - .also { wb -> wb.status = PEER_NODE_STATUS } + .also { wb -> wb.status = status } .build() .encode() .toByteString() @@ -787,54 +841,14 @@ class MockRadioTransport( transportScope.handledLaunch { delay(ACK_DELAY_MS) lifecycle.runIfOpen { - callback.handleFromRadio(makeAck(SIM_PEERS[DIRECT_PEER_INDEX].num, packet.from, packet.id).encode()) + val directPeer = scenario.peers[scenario.directPeerIndex] + callback.handleFromRadio(makeAck(directPeer.num, packet.from, packet.id).encode()) } } } - /** One simulated peer in the demo mesh. */ - private data class SimPeer( - val num: Int, - val longName: String, - val shortName: String, - val hwModel: HardwareModel, - val role: Config.DeviceConfig.Role, - val latitude: Double, - val longitude: Double, - val altitude: Int, - val batteryLevel: Int, - val voltage: Float, - val snr: Float, - val rssi: Int, - val hops: Int, - val secondsSinceHeard: Int, - val uptimeSeconds: Int, - ) - - /** Latitude/longitude/altitude triple, converted to the proto's scaled-integer representation on demand. */ - private data class SimPosition(val latitude: Double, val longitude: Double, val altitude: Int) { - fun toProto() = ProtoPosition.Builder() - .also { wb -> - wb.latitude_i = org.meshtastic.core.model.Position.degI(latitude) - wb.longitude_i = org.meshtastic.core.model.Position.degI(longitude) - wb.altitude = altitude - wb.time = nowSeconds.toInt() - // 32 bits is "full precision"; the coarse end of the scale draws a large uncertainty circle instead - // of - // placing the node where it actually is. - wb.precision_bits = 32 - wb.sats_in_view = 9 - wb.location_source = ProtoPosition.LocSource.LOC_INTERNAL - } - .build() - } - private companion object { - const val MY_NODE = 0x42424242 const val BROADCAST_ADDR = -1 // 0xffffffff - const val FIRMWARE_VERSION = "9.9.9.abcdefg" - const val MY_NODE_STATUS = "Running Demo Mode — no radio attached." - const val PEER_NODE_STATUS = "Solar powered, up on the ridge." const val AUTO_REPLY_TEXT = "Got it, thanks! Message received on the demo mesh." /** First packet id handed out; low enough to stay clear of ids the app generates for its own sends. */ @@ -851,9 +865,6 @@ class MockRadioTransport( /** Hop budget the simulated nodes transmit with; `hop_limit` is derived so the app can infer hop distance. */ const val DEFAULT_HOP_START = 3 - const val DIRECT_PEER_INDEX = 0 - const val TELEMETRY_PEER_COUNT = 4 - val WEATHER_PEER_INDEXES = listOf(4) /** Spacing between seeded frames; the app timestamps rows on persist, so a burst would collapse together. */ const val SEED_SPACING_MS = 120L @@ -864,168 +875,5 @@ class MockRadioTransport( const val ACK_DELAY_MS = 2_000L val FAKE_SESSION_PASSKEY: okio.ByteString = okio.ByteString.of(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77) - - val MY_POSITION = SimPosition(latitude = 32.776665, longitude = -96.796989, altitude = 138) - - /** - * The demo mesh. Deterministic on purpose — the same mesh every launch makes the demo reproducible for - * screenshots, support requests and store reviews. Spread over ~15 km so the map has something to fit. - */ - val SIM_PEERS = - listOf( - SimPeer( - num = MY_NODE + 1, - longName = "Riverside Base", - shortName = "RVSD", - hwModel = HardwareModel.HELTEC_V3, - role = Config.DeviceConfig.Role.CLIENT, - latitude = 32.802, - longitude = -96.769, - altitude = 152, - batteryLevel = 92, - voltage = 4.09f, - snr = 11.5f, - rssi = -62, - hops = 0, - secondsSinceHeard = 45, - uptimeSeconds = 128_400, - ), - SimPeer( - num = MY_NODE + 2, - longName = "Trail Runner", - shortName = "TRLR", - hwModel = HardwareModel.TRACKER_T1000_E, - role = Config.DeviceConfig.Role.TRACKER, - latitude = 32.7605, - longitude = -96.8305, - altitude = 145, - batteryLevel = 64, - voltage = 3.87f, - snr = 6.25f, - rssi = -84, - hops = 0, - secondsSinceHeard = 130, - uptimeSeconds = 41_900, - ), - SimPeer( - num = MY_NODE + 3, - longName = "Oak Cliff Repeater", - shortName = "OAKR", - hwModel = HardwareModel.RAK4631, - role = Config.DeviceConfig.Role.ROUTER, - latitude = 32.7395, - longitude = -96.8215, - altitude = 189, - batteryLevel = 100, - voltage = 4.14f, - snr = 9.0f, - rssi = -71, - hops = 0, - secondsSinceHeard = 20, - uptimeSeconds = 903_600, - ), - SimPeer( - num = MY_NODE + 4, - longName = "Deep Ellum Handheld", - shortName = "DEEP", - hwModel = HardwareModel.T_DECK, - role = Config.DeviceConfig.Role.CLIENT, - latitude = 32.7842, - longitude = -96.7845, - altitude = 141, - batteryLevel = 47, - voltage = 3.74f, - snr = -2.5f, - rssi = -103, - hops = 1, - secondsSinceHeard = 320, - uptimeSeconds = 9_800, - ), - SimPeer( - num = MY_NODE + 5, - longName = "Rooftop Weather", - shortName = "WTHR", - hwModel = HardwareModel.HELTEC_MESH_NODE_T114, - role = Config.DeviceConfig.Role.SENSOR, - latitude = 32.8145, - longitude = -96.8055, - altitude = 205, - batteryLevel = 88, - voltage = 4.02f, - snr = 4.75f, - rssi = -91, - hops = 1, - secondsSinceHeard = 210, - uptimeSeconds = 512_000, - ), - SimPeer( - num = MY_NODE + 6, - longName = "Lakeside Solar", - shortName = "LAKE", - hwModel = HardwareModel.STATION_G2, - role = Config.DeviceConfig.Role.CLIENT, - latitude = 32.8365, - longitude = -96.7325, - altitude = 167, - batteryLevel = 73, - voltage = 3.94f, - snr = 1.5f, - rssi = -98, - hops = 2, - secondsSinceHeard = 640, - uptimeSeconds = 254_300, - ), - SimPeer( - num = MY_NODE + 7, - longName = "Bike Courier", - shortName = "BIKE", - hwModel = HardwareModel.TBEAM, - role = Config.DeviceConfig.Role.TRACKER, - latitude = 32.7688, - longitude = -96.7492, - altitude = 134, - batteryLevel = 31, - voltage = 3.62f, - snr = -6.5f, - rssi = -112, - hops = 2, - secondsSinceHeard = 1_180, - uptimeSeconds = 3_600, - ), - SimPeer( - num = MY_NODE + 8, - longName = "Field Kit Echo", - shortName = "ECHO", - hwModel = HardwareModel.T_ECHO, - role = Config.DeviceConfig.Role.CLIENT_MUTE, - latitude = 32.7215, - longitude = -96.7738, - altitude = 158, - batteryLevel = 56, - voltage = 3.81f, - snr = 3.25f, - rssi = -95, - hops = 1, - secondsSinceHeard = 2_400, - uptimeSeconds = 76_500, - ), - ) - - /** Seeded channel conversation, as (peer index, text) pairs. Oldest first. */ - val CHANNEL_CONVERSATION = - listOf( - 0 to "Morning all — base station is back online after the power cut.", - 2 to "Copy that. Repeater on Oak Cliff is holding steady, 100% battery.", - 1 to "Out on the trail loop, signal is solid the whole way today.", - 4 to "Rooftop sensor reading 18.5C and 47% humidity if anyone cares.", - 0 to "Nice. Net check complete, everyone reporting in.", - ) - - /** Seeded direct-message thread from [DIRECT_PEER_INDEX]. Oldest first. */ - val DIRECT_CONVERSATION = - listOf( - "Hey, are you still planning to bring the spare antenna tomorrow?", - "No rush — just let me know before you set off.", - ) } } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockScenario.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockScenario.kt new file mode 100644 index 0000000000..2717347b98 --- /dev/null +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockScenario.kt @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.network.radio + +import org.meshtastic.core.common.util.nowSeconds +import org.meshtastic.proto.Config +import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.Position as ProtoPosition + +/** + * The mesh a [MockRadioTransport] simulates: our node, its peers, and the history it seeds on connect. The address + * suffix after `m` picks it, so a scenario other than [DEMO] is reachable only by naming it, never from the picker. + */ +@Suppress("detekt:MagicNumber") +internal data class MockScenario( + val myNode: Int, + val longName: String, + val shortName: String, + val hwModel: HardwareModel, + val firmwareVersion: String, + val nodeStatus: String, + val position: SimPosition, + val peers: List, + /** (index into [peers], text) pairs, oldest first. */ + val channelConversation: List>, + val directPeerIndex: Int, + /** Texts from [directPeerIndex] to us, oldest first. */ + val directConversation: List, + /** The first peers, which report device telemetry on connect and, with [liveTelemetry], every tick after. */ + val telemetryPeerCount: Int, + /** Keep reporting telemetry after the seed pass. Off where a stable picture matters more than a live one. */ + val liveTelemetry: Boolean, + /** 101 is what firmware reports with no battery or while charging. */ + val myBatteryLevel: Int = 78, + /** Null without a battery, which firmware leaves off the wire. */ + val myVoltage: Float? = 3.98f, +) { + companion object { + /** The suffix that selects [SHOWCASE]: `mshowcase`. */ + const val SHOWCASE_ADDRESS = "showcase" + + fun forAddress(address: String): MockScenario = if (address == SHOWCASE_ADDRESS) SHOWCASE else DEMO + + private const val DEMO_NODE = 0x42424242 + + /** + * Demo Mode's mesh. Deterministic on purpose, so the demo is the same every launch for support requests and + * store reviews. Spread over ~15 km so the map has something to fit. + */ + val DEMO = + MockScenario( + myNode = DEMO_NODE, + longName = "Demo Handset", + shortName = "DEMO", + hwModel = HardwareModel.ANDROID_SIM, + firmwareVersion = "9.9.9.abcdefg", + nodeStatus = "Running Demo Mode — no radio attached.", + position = SimPosition(latitude = 32.776665, longitude = -96.796989, altitude = 138), + peers = + listOf( + SimPeer( + num = DEMO_NODE + 1, + longName = "Riverside Base", + shortName = "RVSD", + hwModel = HardwareModel.HELTEC_V3, + role = Config.DeviceConfig.Role.CLIENT, + latitude = 32.802, + longitude = -96.769, + altitude = 152, + batteryLevel = 92, + voltage = 4.09f, + snr = 11.5f, + rssi = -62, + hops = 0, + secondsSinceHeard = 45, + uptimeSeconds = 128_400, + ), + SimPeer( + num = DEMO_NODE + 2, + longName = "Trail Runner", + shortName = "TRLR", + hwModel = HardwareModel.TRACKER_T1000_E, + role = Config.DeviceConfig.Role.TRACKER, + latitude = 32.7605, + longitude = -96.8305, + altitude = 145, + batteryLevel = 64, + voltage = 3.87f, + snr = 6.25f, + rssi = -84, + hops = 0, + secondsSinceHeard = 130, + uptimeSeconds = 41_900, + status = "Solar powered, up on the ridge.", + ), + SimPeer( + num = DEMO_NODE + 3, + longName = "Oak Cliff Repeater", + shortName = "OAKR", + hwModel = HardwareModel.RAK4631, + role = Config.DeviceConfig.Role.ROUTER, + latitude = 32.7395, + longitude = -96.8215, + altitude = 189, + batteryLevel = 100, + voltage = 4.14f, + snr = 9.0f, + rssi = -71, + hops = 0, + secondsSinceHeard = 20, + uptimeSeconds = 903_600, + ), + SimPeer( + num = DEMO_NODE + 4, + longName = "Deep Ellum Handheld", + shortName = "DEEP", + hwModel = HardwareModel.T_DECK, + role = Config.DeviceConfig.Role.CLIENT, + latitude = 32.7842, + longitude = -96.7845, + altitude = 141, + batteryLevel = 47, + voltage = 3.74f, + snr = -2.5f, + rssi = -103, + hops = 1, + secondsSinceHeard = 320, + uptimeSeconds = 9_800, + ), + SimPeer( + num = DEMO_NODE + 5, + longName = "Rooftop Weather", + shortName = "WTHR", + hwModel = HardwareModel.HELTEC_MESH_NODE_T114, + role = Config.DeviceConfig.Role.SENSOR, + latitude = 32.8145, + longitude = -96.8055, + altitude = 205, + batteryLevel = 88, + voltage = 4.02f, + snr = 4.75f, + rssi = -91, + hops = 1, + secondsSinceHeard = 210, + uptimeSeconds = 512_000, + environment = + SimEnvironment( + temperature = 18.5f, + relativeHumidity = 47f, + barometricPressure = 1013.2f, + ), + ), + SimPeer( + num = DEMO_NODE + 6, + longName = "Lakeside Solar", + shortName = "LAKE", + hwModel = HardwareModel.STATION_G2, + role = Config.DeviceConfig.Role.CLIENT, + latitude = 32.8365, + longitude = -96.7325, + altitude = 167, + batteryLevel = 73, + voltage = 3.94f, + snr = 1.5f, + rssi = -98, + hops = 2, + secondsSinceHeard = 640, + uptimeSeconds = 254_300, + ), + SimPeer( + num = DEMO_NODE + 7, + longName = "Bike Courier", + shortName = "BIKE", + hwModel = HardwareModel.TBEAM, + role = Config.DeviceConfig.Role.TRACKER, + latitude = 32.7688, + longitude = -96.7492, + altitude = 134, + batteryLevel = 31, + voltage = 3.62f, + snr = -6.5f, + rssi = -112, + hops = 2, + secondsSinceHeard = 1_180, + uptimeSeconds = 3_600, + ), + SimPeer( + num = DEMO_NODE + 8, + longName = "Field Kit Echo", + shortName = "ECHO", + hwModel = HardwareModel.T_ECHO, + role = Config.DeviceConfig.Role.CLIENT_MUTE, + latitude = 32.7215, + longitude = -96.7738, + altitude = 158, + batteryLevel = 56, + voltage = 3.81f, + snr = 3.25f, + rssi = -95, + hops = 1, + secondsSinceHeard = 2_400, + uptimeSeconds = 76_500, + ), + ), + channelConversation = + listOf( + 0 to "Morning all — base station is back online after the power cut.", + 2 to "Copy that. Repeater on Oak Cliff is holding steady, 100% battery.", + 1 to "Out on the trail loop, signal is solid the whole way today.", + 4 to "Rooftop sensor reading 18.5C and 47% humidity if anyone cares.", + 0 to "Nice. Net check complete, everyone reporting in.", + ), + directPeerIndex = 0, + directConversation = + listOf( + "Hey, are you still planning to bring the spare antenna tomorrow?", + "No rush — just let me know before you set off.", + ), + telemetryPeerCount = 4, + liveTelemetry = true, + ) + + private const val SHOWCASE_NODE = 0xb22c94ef.toInt() + private const val SHOWCASE_LAT = 32.7767 + private const val SHOWCASE_LON = -96.797 + + /** A peer [minutes] ago, 30 s into that minute so a slow frame cannot tip a "3 min" label over. */ + private fun heard(minutes: Int): Int = if (minutes == 0) 5 else minutes * 60 + 30 + + /** + * The store listing's mesh: a hiking group around a base camp, every node named for a place or a person, and a + * channel thread that tells one morning. Each peer's number is the CRC-32 of its public key, as firmware 2.8 + * derives it, and the keys give every node a distinct avatar colour. Static after the seed pass, so every + * capture shows the same numbers. + */ + val SHOWCASE = + MockScenario( + myNode = SHOWCASE_NODE, + longName = "Base Camp", + shortName = "BASE", + hwModel = HardwareModel.HELTEC_V3, + firmwareVersion = "2.7.26.54e0d8d", + nodeStatus = "Base camp radio, on grid power.", + position = SimPosition(latitude = SHOWCASE_LAT, longitude = SHOWCASE_LON, altitude = 150), + peers = + listOf( + SimPeer( + num = 0xe1e22a35.toInt(), + longName = "Ridge Top", + shortName = "RDGE", + hwModel = HardwareModel.RAK4631, + role = Config.DeviceConfig.Role.ROUTER, + latitude = SHOWCASE_LAT + 0.04, + longitude = SHOWCASE_LON + 0.03, + altitude = 214, + batteryLevel = 88, + voltage = 4.04f, + snr = 10.5f, + rssi = -86, + hops = 0, + secondsSinceHeard = heard(2), + uptimeSeconds = 19 * 86_400 + 7 * 3_600, + publicKey = "836124f1bec84c1145fa1c46ae436b8ded03b14f346c36f99144bd6ae10da527", + channelUtilization = 14.6f, + airUtilTx = 3.1f, + environment = + SimEnvironment( + temperature = 18.0f, + relativeHumidity = 66f, + barometricPressure = 990.9f, + ), + status = "Relay for the valley trails.", + favorite = true, + ), + SimPeer( + num = 0x13f09802, + longName = "Summit Solar", + shortName = "SMMT", + hwModel = HardwareModel.RAK4631, + role = Config.DeviceConfig.Role.ROUTER, + latitude = SHOWCASE_LAT + 0.05, + longitude = SHOWCASE_LON - 0.04, + altitude = 249, + batteryLevel = 101, + voltage = 4.14f, + snr = 3.0f, + rssi = -109, + hops = 2, + secondsSinceHeard = heard(15), + uptimeSeconds = 63 * 86_400 + 2 * 3_600, + publicKey = "469c03ba5432902d3a8404ce80b06354982bce1e7004b2a9872db6ec988e1b69", + channelUtilization = 12.2f, + airUtilTx = 2.4f, + environment = + SimEnvironment( + temperature = 19.6f, + relativeHumidity = 61f, + barometricPressure = 986.8f, + voltage = 5.71f, + current = 186f, + ), + status = "Solar powered, up on the summit.", + ), + SimPeer( + num = 0xbf4f9846.toInt(), + longName = "Trailhead", + shortName = "TRLH", + hwModel = HardwareModel.TBEAM, + role = Config.DeviceConfig.Role.CLIENT, + latitude = SHOWCASE_LAT - 0.03, + longitude = SHOWCASE_LON + 0.045, + altitude = 231, + batteryLevel = 72, + voltage = 3.91f, + snr = 7.75f, + rssi = -97, + hops = 1, + secondsSinceHeard = heard(4), + uptimeSeconds = 6 * 3_600, + publicKey = "81c7cb197b6e047c7fdf0a262cbe9374bdc81ecbdcd5d63357d5a962544fe673", + channelUtilization = 8.9f, + airUtilTx = 1.1f, + ), + SimPeer( + num = 0xe69233a3.toInt(), + longName = "Sarah's Truck", + shortName = "SRAH", + hwModel = HardwareModel.T_DECK, + role = Config.DeviceConfig.Role.CLIENT, + latitude = SHOWCASE_LAT + 0.025, + longitude = SHOWCASE_LON + 0.01, + altitude = 176, + batteryLevel = 83, + voltage = 4.01f, + snr = 8.5f, + rssi = -94, + hops = 0, + secondsSinceHeard = heard(8), + uptimeSeconds = 4 * 3_600, + publicKey = "50dd259c31af84f61ec364c8759681d63279248503370a8a9b91b49900eae451", + channelUtilization = 7.4f, + airUtilTx = 0.9f, + favorite = true, + ), + SimPeer( + num = 0x3804847e, + longName = "River Crossing", + shortName = "RIVR", + hwModel = HardwareModel.T_ECHO, + role = Config.DeviceConfig.Role.CLIENT_MUTE, + latitude = SHOWCASE_LAT - 0.045, + longitude = SHOWCASE_LON - 0.02, + altitude = 128, + batteryLevel = 64, + voltage = 3.84f, + snr = 5.25f, + rssi = -104, + hops = 1, + secondsSinceHeard = heard(7), + uptimeSeconds = 2 * 86_400, + publicKey = "a02b20ee4fd5863190277fab08953bfd685bac345f30ad5a537a690f3425e978", + channelUtilization = 6.8f, + airUtilTx = 0.3f, + ), + SimPeer( + num = 0x07f9e628, + longName = "Ham Shack", + shortName = "SHCK", + hwModel = HardwareModel.STATION_G2, + role = Config.DeviceConfig.Role.CLIENT, + latitude = SHOWCASE_LAT - 0.02, + longitude = SHOWCASE_LON - 0.035, + altitude = 162, + batteryLevel = 101, + voltage = null, + snr = 4.5f, + rssi = -104, + hops = 2, + secondsSinceHeard = heard(11), + uptimeSeconds = 41 * 86_400, + publicKey = "ab68a145efd254a18457923ceb699c72954b76f95a2ecf8f2d4657afb571f07f", + channelUtilization = 9.7f, + airUtilTx = 1.8f, + environment = + SimEnvironment( + temperature = 22.6f, + relativeHumidity = 46f, + barometricPressure = 997.0f, + iaq = 44, + ), + status = "On mains, listening 24/7.", + ), + SimPeer( + num = 0x9dd51959.toInt(), + longName = "Kayak Dan", + shortName = "KDAN", + hwModel = HardwareModel.HELTEC_WIRELESS_TRACKER, + role = Config.DeviceConfig.Role.TRACKER, + latitude = SHOWCASE_LAT + 0.012, + longitude = SHOWCASE_LON - 0.042, + altitude = 120, + batteryLevel = 58, + voltage = 3.78f, + snr = 2.0f, + rssi = -110, + hops = 2, + secondsSinceHeard = heard(4), + uptimeSeconds = 3 * 3_600, + publicKey = "a170af3ac2fd3ff64be3e90a8c98383fe950ce94e9afdea73a61d04d03dfdb25", + channelUtilization = 5.1f, + airUtilTx = 1.3f, + ), + SimPeer( + num = 0xb64352a4.toInt(), + longName = "Old Fire Lookout", + shortName = "LOOK", + hwModel = HardwareModel.TBEAM, + role = Config.DeviceConfig.Role.CLIENT, + latitude = SHOWCASE_LAT - 0.05, + longitude = SHOWCASE_LON + 0.04, + altitude = 268, + batteryLevel = 47, + voltage = 3.69f, + snr = -6.0f, + rssi = -118, + hops = 3, + secondsSinceHeard = heard(60), + uptimeSeconds = 9 * 86_400, + publicKey = "fb30f6029d4924b2d6a32d464df0b2b544e31e8956f8ea758045621fa78a6c5f", + channelUtilization = 4.3f, + airUtilTx = 0.4f, + environment = + SimEnvironment( + temperature = 17.6f, + relativeHumidity = 67f, + barometricPressure = 984.5f, + ), + ), + ), + channelConversation = + listOf( + 2 to "Heading up from the trailhead now, 4 of us", + 3 to "Parked at the overflow lot, radio on", + 2 to "Made the ridge, good signal back to base", + 6 to "Water at the crossing is low, safe to ford", + 3 to "Bringing the truck around to the lower lot at 3", + 2 to "Lunch at the lookout, back on the air in 30", + ), + directPeerIndex = 6, + directConversation = listOf("Paddling past the crossing now.", "Can you see me on the map yet?"), + telemetryPeerCount = 8, + liveTelemetry = false, + myBatteryLevel = 101, + myVoltage = null, + ) + } +} + +internal data class SimPeer( + val num: Int, + val longName: String, + val shortName: String, + val hwModel: HardwareModel, + val role: Config.DeviceConfig.Role, + val latitude: Double, + val longitude: Double, + val altitude: Int, + /** 101 is what firmware reports with no battery or while charging. */ + val batteryLevel: Int, + /** On firmware's LiPo curve for [batteryLevel]; null without a battery. */ + val voltage: Float?, + val snr: Float, + val rssi: Int, + val hops: Int, + val secondsSinceHeard: Int, + val uptimeSeconds: Int, + /** Hex X25519 public key, whose CRC-32 is [num] on firmware 2.8. */ + val publicKey: String? = null, + val channelUtilization: Float? = null, + val airUtilTx: Float? = null, + /** Reported on connect, and by the first such peer every few ticks under [MockScenario.liveTelemetry]. */ + val environment: SimEnvironment? = null, + val status: String? = null, + val favorite: Boolean = false, +) + +/** An environment sensor's readings; [voltage] and [current] are an INA power monitor's. */ +internal data class SimEnvironment( + val temperature: Float, + val relativeHumidity: Float, + val barometricPressure: Float, + val iaq: Int? = null, + val voltage: Float? = null, + val current: Float? = null, +) + +/** Latitude/longitude/altitude triple, converted to the proto's scaled-integer representation on demand. */ +@Suppress("detekt:MagicNumber") +internal data class SimPosition(val latitude: Double, val longitude: Double, val altitude: Int) { + fun toProto() = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = org.meshtastic.core.model.Position.degI(latitude) + wb.longitude_i = org.meshtastic.core.model.Position.degI(longitude) + wb.altitude = altitude + wb.time = nowSeconds.toInt() + // 32 bits is "full precision"; the coarse end of the scale draws a large uncertainty circle instead of + // placing the node where it actually is. + wb.precision_bits = 32 + wb.sats_in_view = 9 + wb.location_source = ProtoPosition.LocSource.LOC_INTERNAL + } + .build() +} diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt index 7b8c7f4ef2..fea50b0540 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt @@ -215,7 +215,7 @@ class ReplayRadioTransport( /** Reads a big-endian `u32` as a non-negative [Int], rejecting truncation and the >2 GiB high-bit range. */ private fun Buffer.readUInt32(label: String): Int { - if (size < INT_BYTES) throw IllegalArgumentException("Malformed replay asset: truncated $label") + require(size >= INT_BYTES) { "Malformed replay asset: truncated $label" } val value = try { readInt() diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt index 2764a28b7b..b62d9b4a71 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt @@ -52,12 +52,17 @@ abstract class StreamTransport(protected val callback: RadioTransportCallback, p private val codec = StreamFrameCodec(onPacketReceived = { callback.handleFromRadio(it) }, logTag = "StreamTransport") private val sendQueue = Channel(capacity = MAX_PENDING_SENDS) + + // A send's failure is inspected below: the worker's own cancellation is rethrown, a stray one only logged. + @Suppress("SuspendFunSwallowedCancellation") private val sendWorker = scope .handledLaunch { for (send in sendQueue) { - val failure = - runCatching { codec.frameAndSend(send.payload, send.writer, send.flusher) }.exceptionOrNull() + val failure = runCatching { + codec.frameAndSend(send.payload, send.writer, send.flusher) + } + .exceptionOrNull() try { when (failure) { null -> Unit diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt index 3fcc197900..dd0431bf5b 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.withTimeoutOrNull import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.util.anonymizePublicHost import org.meshtastic.core.network.transport.StreamFrameCodec import org.meshtastic.core.network.transport.TcpTransport import org.meshtastic.core.repository.RadioTransport @@ -88,7 +89,7 @@ internal constructor( dispatchers = dispatchers, scope = scope, listener = listener, - logTag = "TcpRadioTransport[$address]", + logTag = "TcpRadioTransport[${address.anonymizePublicHost()}]", ), ) }, @@ -125,7 +126,10 @@ internal constructor( override fun start() { lifecycle.runIfOpen { if (transportStopped.value) { - Logger.w { "[$address] Ignoring start on a stopped TCP transport; a fresh transport is required" } + Logger.w { + "[${address.anonymizePublicHost()}] Ignoring start on a stopped TCP transport; " + + "a fresh transport is required" + } } else { transport.start(address) } @@ -133,7 +137,7 @@ internal constructor( } override suspend fun close() { - Logger.d { "[$address] Closing TCP transport" } + Logger.d { "[${address.anonymizePublicHost()}] Closing TCP transport" } val completed = lifecycle.close( teardown = { @@ -141,14 +145,16 @@ internal constructor( cancelOutstandingOperations() }, ) - if (!completed) Logger.w { "[$address] TCP teardown did not complete within its lifecycle bounds" } + if (!completed) { + Logger.w { "[${address.anonymizePublicHost()}] TCP teardown did not complete within its lifecycle bounds" } + } // Do NOT emit onDisconnect(isPermanent = true) here. The explicit-disconnect signal is the service layer's // responsibility (SharedRadioInterfaceService.stopTransportLocked); emitting it here causes a double-disconnect // and prevents the auto-reconnect loop from owning its transient lifecycle. } override fun keepAlive() { - Logger.d { "[$address] TCP keepAlive" } + Logger.d { "[${address.anonymizePublicHost()}] TCP keepAlive" } launchConnectionOperation("heartbeat") { transport.sendHeartbeat() } } @@ -173,7 +179,9 @@ internal constructor( scope.handledLaunch { val completed = withTimeoutOrNull(OPERATION_TIMEOUT) { block() } != null if (!completed) { - Logger.w { "[$address] TCP $operation timed out after $OPERATION_TIMEOUT" } + Logger.w { + "[${address.anonymizePublicHost()}] TCP $operation timed out after $OPERATION_TIMEOUT" + } // Cancellation may leave a framed write partially emitted. Stopping this one-shot // transport forces the service reconnect path to create a fresh transport before another // send. @@ -197,7 +205,9 @@ internal constructor( val jobs = synchronized(operationJobsLock) { operationJobs.toList() } jobs.forEach { it.cancel() } val joined = withTimeoutOrNull(OPERATION_TIMEOUT) { jobs.joinAll() } != null - if (!joined) Logger.w { "[$address] TCP operation jobs did not stop after transport teardown" } + if (!joined) { + Logger.w { "[${address.anonymizePublicHost()}] TCP operation jobs did not stop after transport teardown" } + } } private fun stopTransport() { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGate.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGate.kt index 389d04007a..6027fc25e2 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGate.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGate.kt @@ -89,7 +89,8 @@ internal class TransportLifecycleGate( * complete within their bounds. A phase failure is recorded on the shared completion and rethrown to every closer; * the gate is deliberately not reopened or retried after a poisoned close. */ - @Suppress("TooGenericExceptionCaught") + // Every failure, cancellation included, is recorded on the shared completion and then rethrown. + @Suppress("TooGenericExceptionCaught", "SuspendFunSwallowedCancellation") suspend fun close(beforeDrain: suspend () -> Unit = {}, teardown: suspend () -> Unit = {}): Boolean = withContext(NonCancellable) { val plan = closePlan() diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt index 023c9b85e1..f6970a2da0 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.network.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.meshtastic.mqtt.ConnectionState +import org.meshtastic.mqtt.MqttException import org.meshtastic.proto.MqttClientProxyMessage /** Interface defining the MQTT interactions used for proxying messages to and from the mesh. */ @@ -43,4 +44,10 @@ interface MQTTRepository { /** Observable MQTT connection lifecycle state (DISCONNECTED → CONNECTING → CONNECTED → RECONNECTING). */ val connectionState: StateFlow + + /** + * The broker's refusal of one or more topic filters on the current connection, or `null` when every filter was + * granted or nothing has been subscribed yet. Cleared when the session ends. + */ + val subscriptionRefusal: StateFlow } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 398be02257..a701a81ce8 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -48,6 +48,7 @@ import org.koin.core.annotation.Single import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.MqttJsonPayload +import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.subscribeList import org.meshtastic.core.repository.NodeRepository @@ -58,6 +59,7 @@ import org.meshtastic.mqtt.MqttEndpoint import org.meshtastic.mqtt.MqttException import org.meshtastic.mqtt.MqttLogLevel import org.meshtastic.mqtt.MqttMessage +import org.meshtastic.mqtt.MqttProtocolVersion import org.meshtastic.mqtt.QoS import org.meshtastic.mqtt.ReasonCode import org.meshtastic.mqtt.packet.Subscription @@ -112,6 +114,11 @@ class MQTTRepositoryImpl( .onEach(::logConnectionState) .stateIn(scope, SharingStarted.Eagerly, ConnectionState.Disconnected.Idle) + override val subscriptionRefusal: StateFlow = + activeSession + .flatMapLatest { session -> session?.subscriptionRefusal ?: flowOf(null) } + .stateIn(scope, SharingStarted.Eagerly, null) + @OptIn(ExperimentalSerializationApi::class) private val json = Json { ignoreUnknownKeys = true @@ -154,34 +161,6 @@ class MQTTRepositoryImpl( val session = ActiveMqttSession(newClient) closeSession(replaceActiveSession(session)) - val subscriptions: List = buildList { - channelSet.subscribeList.forEach { globalId -> - add( - Subscription( - "$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+", - maxQos = QoS.AT_LEAST_ONCE, - noLocal = true, - ), - ) - if (mqttConfig?.json_enabled == true) { - add( - Subscription( - "$rootTopic$JSON_TOPIC_LEVEL$globalId/+", - maxQos = QoS.AT_LEAST_ONCE, - noLocal = true, - ), - ) - } - } - add( - Subscription( - "$rootTopic$DEFAULT_TOPIC_LEVEL$PKI_CHANNEL_ID/+", - maxQos = QoS.AT_LEAST_ONCE, - noLocal = true, - ), - ) - } - // Collect from the SharedFlow before connecting to avoid missing retained messages // that arrive immediately after SUBSCRIBE. launch { newClient.messages.collect { msg -> processMessage(msg) } } @@ -199,9 +178,17 @@ class MQTTRepositoryImpl( } newClient.connect(endpoint) if (!isActiveSession(session)) return@launch + // Built here, not before connect: the option set depends on the version the broker accepted. + val subscriptions = + buildSubscriptions( + globalIds = channelSet.subscribeList, + rootTopic = rootTopic, + jsonEnabled = mqttConfig?.json_enabled == true, + version = newClient.negotiatedProtocolVersion, + ) if (subscriptions.isNotEmpty()) { Logger.d { "MQTT subscribing to ${subscriptions.size} topics" } - newClient.subscribe(subscriptions) + subscribe(session, subscriptions) } Logger.i { "MQTT connected and subscribed" } } @@ -251,6 +238,23 @@ class MQTTRepositoryImpl( } } + // A refusal is the broker's verdict on a filter, not a failed attempt: the client keeps the filters it granted, + // and retrying the connect would only draw the same SUBACK on a backoff for ever. Record it for the UI instead. + private suspend fun subscribe(session: ActiveMqttSession, subscriptions: List) { + try { + session.client.subscribe(subscriptions) + } catch (e: MqttException.SubscriptionRefused) { + Logger.w { + if (buildConfigProvider.isDebug) { + "MQTT broker refused ${e.refused.size} of ${subscriptions.size} topics: ${e.message}" + } else { + "MQTT broker refused ${e.refused.size} of ${subscriptions.size} topics" + } + } + session.subscriptionRefusal.value = e + } + } + private fun replaceActiveSession(replacement: ActiveMqttSession): ActiveMqttSession? { while (true) { val current = activeSession.value @@ -293,6 +297,53 @@ class MQTTRepositoryImpl( } } + /** + * Builds the SUBSCRIBE list for the protocol version the broker actually accepted. + * + * `noLocal` stops the broker echoing our own uplink back to us, but it is an MQTT 5 subscription option. The client + * offers 5.0 and silently falls back to 3.1.1 when the broker refuses, and mqtt-client then rejects the whole + * SUBSCRIBE with `IllegalArgumentException`. Because the failure is deterministic, the connect loop retried it + * forever: 121,003 reported errors across 72 users in fourteen days, with MQTT never once working for them. + * + * On 3.1.1 the broker hands our own traffic back instead, which [classifyMqttSelfTraffic] sorts out. No + * subscription at all is strictly worse. + */ + private fun buildSubscriptions( + globalIds: List, + rootTopic: String, + jsonEnabled: Boolean, + version: MqttProtocolVersion, + ): List { + val noLocal = version == MqttProtocolVersion.V5_0 + return buildList { + globalIds.forEach { globalId -> + add( + Subscription( + "$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+", + maxQos = QoS.AT_LEAST_ONCE, + noLocal = noLocal, + ), + ) + if (jsonEnabled) { + add( + Subscription( + "$rootTopic$JSON_TOPIC_LEVEL$globalId/+", + maxQos = QoS.AT_LEAST_ONCE, + noLocal = noLocal, + ), + ) + } + } + add( + Subscription( + "$rootTopic$DEFAULT_TOPIC_LEVEL$PKI_CHANNEL_ID/+", + maxQos = QoS.AT_LEAST_ONCE, + noLocal = noLocal, + ), + ) + } + } + @OptIn(ExperimentalSerializationApi::class) private fun ProducerScope.processMessage(msg: MqttMessage) { val topic = msg.topic @@ -301,16 +352,21 @@ class MQTTRepositoryImpl( if (topic.contains("/json/")) { try { val jsonStr = payload.decodeToString() - json.decodeFromString(jsonStr) - trySend( - MqttClientProxyMessage.Builder() - .also { wb -> - wb.topic = topic - wb.text = jsonStr - wb.retained = msg.retain - } - .build(), - ) + val decoded = json.decodeFromString(jsonStr) + val selfTraffic = classifyMqttJsonSelfTraffic(decoded.from, decoded.sender, nodeRepository.myId.value) + if (selfTraffic != MqttSelfTraffic.FORWARD) { + Logger.d { "MQTT json dropped ($selfTraffic)" } + } else { + trySend( + MqttClientProxyMessage.Builder() + .also { wb -> + wb.topic = topic + wb.text = jsonStr + wb.retained = msg.retain + } + .build(), + ) + } } catch (e: JsonDecodingException) { // Warn, not error: a non-conforming payload recurs for as long as a busy public broker // stays subscribed, not an app defect worth a non-fatal per message. @@ -321,29 +377,41 @@ class MQTTRepositoryImpl( Logger.w(e) { "Failed to parse MQTT JSON: ${e.message}" } } } else { - // Drop provably-undeliverable downlink packets before spending BLE bandwidth on - // them. In client-proxy mode the public broker floods payload-less packet-header - // stubs the node can only decrypt-fail and discard; stopping them here saves BLE - // airtime, a decrypt attempt, and a node-side warning per packet. - if (isUndeliverableDownlink(payload, nodeRepository.myId.value)) { - Logger.d { - if (buildConfigProvider.isDebug) { - "MQTT downlink dropped (no payload): $topic" - } else { - "MQTT downlink dropped (no payload)" + val myId = nodeRepository.myId.value + val selfTraffic = classifyMqttSelfTraffic(payload, myId) + when { + // A broker that negotiated MQTT 3.1.1 cannot honour `noLocal`, so our own traffic comes straight + // back. Only two of the three self-traffic cases are worth dropping - our own uplink returning is + // what firmware reads as a local ack, so it is relayed. See [classifyMqttSelfTraffic]. + selfTraffic == MqttSelfTraffic.FORGED_SENDER -> + Logger.w { "MQTT packet claiming our node id via another gateway dropped" } + + selfTraffic == MqttSelfTraffic.OWN_UPLINK_ECHO -> Logger.d { "MQTT own-uplink echo dropped" } + + // Drop provably-undeliverable downlink packets before spending BLE bandwidth on + // them. In client-proxy mode the public broker floods payload-less packet-header + // stubs the node can only decrypt-fail and discard; stopping them here saves BLE + // airtime, a decrypt attempt, and a node-side warning per packet. + isUndeliverableDownlink(payload, myId) -> + Logger.d { + if (buildConfigProvider.isDebug) { + "MQTT downlink dropped (no payload): $topic" + } else { + "MQTT downlink dropped (no payload)" + } } - } - return + + else -> + trySend( + MqttClientProxyMessage.Builder() + .also { wb -> + wb.topic = topic + wb.data_ = payload.toByteString() + wb.retained = msg.retain + } + .build(), + ) } - trySend( - MqttClientProxyMessage.Builder() - .also { wb -> - wb.topic = topic - wb.data_ = payload.toByteString() - wb.retained = msg.retain - } - .build(), - ) } } @@ -383,6 +451,7 @@ class MQTTRepositoryImpl( private class ActiveMqttSession(val client: MqttClientSession) { val closeStarted = atomic(false) val connectJob = atomic(null) + val subscriptionRefusal = MutableStateFlow(null) } /** @@ -433,6 +502,9 @@ internal interface MqttClientSession { val messages: Flow val connectionState: StateFlow + /** The version the broker accepted, valid only after [connect] returns. */ + val negotiatedProtocolVersion: MqttProtocolVersion + suspend fun connect(endpoint: MqttEndpoint) suspend fun subscribe(subscriptions: List) @@ -446,6 +518,10 @@ private class DefaultMqttClientSession(private val delegate: MqttClient) : MqttC override val messages: Flow = delegate.messages override val connectionState: StateFlow = delegate.connectionState + // Read through on every access: the client resets it on connect and preserves it across auto-reconnects. + override val negotiatedProtocolVersion: MqttProtocolVersion + get() = delegate.negotiatedProtocolVersion + override suspend fun connect(endpoint: MqttEndpoint) { delegate.connect(endpoint) } @@ -531,7 +607,7 @@ internal fun effectiveCredentials(config: ModuleConfig.MQTTConfig?): Pairfrom` before + * generating an implicit ack, so a peer that publishes a packet carrying our `from` manufactures a delivery receipt + * for a message that was never delivered. + * - **theirs via us** - our own uplink of traffic we bridged, coming back. Relaying it would spend phone-API and BLE + * bandwidth handing the node a packet it just gave us. + * + * This mirrors `MqttFraming.toCanonical` in meshtastic-node-kmp, which is the closest reading of the firmware rules we + * have. Fails open: unparseable bytes, a packet-less envelope, or an unknown local node id all forward as before. + */ +internal fun classifyMqttSelfTraffic(payload: ByteArray, myId: String?): MqttSelfTraffic { + val envelope = myId?.let { ServiceEnvelope.ADAPTER.decodeOrNull(payload) } + val from = envelope?.packet?.from + + return if (myId == null || from == null) { + MqttSelfTraffic.FORWARD + } else { + selfTrafficOf(fromUs = NodeAddress.numToDefaultId(from) == myId, viaUs = envelope.gateway_id == myId) + } +} + +/** The shared decision behind both classifiers, so the two topics cannot drift apart. */ +private fun selfTrafficOf(fromUs: Boolean, viaUs: Boolean): MqttSelfTraffic = when { + fromUs && !viaUs -> MqttSelfTraffic.FORGED_SENDER + !fromUs && viaUs -> MqttSelfTraffic.OWN_UPLINK_ECHO + else -> MqttSelfTraffic.FORWARD +} + +/** + * The JSON-topic counterpart of [classifyMqttSelfTraffic]. `sender` is the gateway id and `from` the originating node. + */ +internal fun classifyMqttJsonSelfTraffic(from: Long, sender: String?, myId: String?): MqttSelfTraffic = + if (myId == null || sender == null) { + MqttSelfTraffic.FORWARD + } else { + selfTrafficOf(fromUs = NodeAddress.numToDefaultId(from.toInt()) == myId, viaUs = sender == myId) + } + /** * Returns true when a downlink [ServiceEnvelope] is provably un-usable by the node and should be dropped before * forwarding over BLE (MQTT client-proxy mode). * * Fails open — returns false (forward) for anything it cannot positively prove undeliverable: unparseable bytes, - * traffic on the `PKI` channel (public-key-encrypted direct messages the node accepts without first decrypting), our - * own echoed-back packets (used as implicit ACKs), a packet-less envelope, or any packet that actually carries a - * payload. The only drop case is Tier 1: a [MeshPacket] with neither `decoded` nor `encrypted` set — no legitimate - * Meshtastic packet is payload-less. + * traffic on the `PKI` channel (public-key-encrypted direct messages the node accepts without first decrypting), a + * packet-less envelope, or any packet that actually carries a payload. Our own uplink returning still reaches this + * function and is guarded here, because firmware reads it as a local ack; [classifyMqttSelfTraffic] has already dropped + * the other two self-traffic cases. The only drop case is Tier 1: a [MeshPacket] with neither `decoded` nor `encrypted` + * set — no legitimate Meshtastic packet is payload-less. * * Extracted as an internal top-level function so [MQTTRepositoryImplTest] can exercise every branch without spinning up * the full repository. diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt index 88b5a3fd41..b8b6877891 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt @@ -17,12 +17,11 @@ package org.meshtastic.core.network.transport import co.touchlab.kermit.Logger +import kotlinx.atomicfu.atomic import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.ExperimentalAtomicApi /** * Shared heartbeat sender for Meshtastic radio transports. @@ -57,8 +56,7 @@ private constructor( rejectionLogger: (HeartbeatRejectionLogLevel, String) -> Unit, ) : this(sendToRadio, afterHeartbeat, logTag, HeartbeatRejectionLogSink(rejectionLogger)) - @OptIn(ExperimentalAtomicApi::class) - private val nonce = AtomicInt(FIRST_NONCE) + private val nonce = atomic(FIRST_NONCE) private val nonceMutex = Mutex() private val rejectionLogPolicy = HeartbeatRejectionLogPolicy() @@ -75,11 +73,10 @@ private constructor( * * @return `true` when the transport accepted the heartbeat handoff. */ - @OptIn(ExperimentalAtomicApi::class) suspend fun sendHeartbeat(): Boolean { val (accepted, rejectionLogLevel) = nonceMutex.withLock { - val n = nonce.load() + val n = nonce.value Logger.v { "[$logTag] Sending ToRadio heartbeat (nonce=$n)" } val admitted = sendToRadio( @@ -88,7 +85,7 @@ private constructor( .build() .encode(), ) - if (admitted) nonce.fetchAndAdd(1) + if (admitted) nonce.incrementAndGet() admitted to rejectionLogPolicy.record(admitted) } if (rejectionLogLevel != null) { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt index b3c1cfb434..9c957e534d 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt @@ -40,6 +40,8 @@ import kotlinx.io.IOException import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.util.TimeConstants +import org.meshtastic.core.model.util.anonymizePublicHost import org.meshtastic.proto.ToRadio import kotlin.concurrent.Volatile @@ -100,7 +102,6 @@ class TcpTransport( /** TCP connect timeout. A failed connect just feeds the reconnect/backoff loop, so it is not fatal. */ const val CONNECT_TIMEOUT_MS = 30_000L private const val READ_BUFFER_SIZE = 1024 - private const val MILLIS_PER_SECOND = 1_000L /** * Minimum session duration for backoff to reset. Sessions shorter than this that ended in peer-EOF are treated @@ -199,7 +200,8 @@ class TcpTransport( // region Connection lifecycle - @Suppress("NestedBlockDepth") + // Cancellation tears the socket down before it is rethrown to end the loop. + @Suppress("NestedBlockDepth", "SuspendFunSwallowedCancellation") private suspend fun connectWithRetry(address: String) { var retryCount = 1 var backoff = MIN_BACKOFF_MILLIS @@ -209,11 +211,11 @@ class TcpTransport( try { connectAndRead(address) } catch (ex: TimeoutCancellationException) { - Logger.w(ex) { "$logTag: [$address] TCP connect timed out" } + Logger.w(ex) { "$logTag: [${address.anonymizePublicHost()}] TCP connect timed out" } disconnectSocket() false } catch (ex: IOException) { - Logger.w(ex) { "$logTag: [$address] TCP connection error" } + Logger.w(ex) { "$logTag: [${address.anonymizePublicHost()}] TCP connection error" } disconnectSocket() false } catch (ce: CancellationException) { @@ -225,9 +227,9 @@ class TcpTransport( // (UnresolvedAddressException, which is an IllegalArgumentException and so misses the IOException // branch above). Log it, retry it, but keep it out of error tracking. if (ex.isExpectedConnectionFailure()) { - Logger.w(ex) { "$logTag: [$address] Radio unreachable" } + Logger.w(ex) { "$logTag: [${address.anonymizePublicHost()}] Radio unreachable" } } else { - Logger.e(ex) { "$logTag: [$address] TCP exception" } + Logger.e(ex) { "$logTag: [${address.anonymizePublicHost()}] TCP exception" } } disconnectSocket() false @@ -238,18 +240,22 @@ class TcpTransport( // growing so the radio has time to recover between reconnect attempts. val sessionUptime = if (connectionStartTime > 0) nowMillis - connectionStartTime else 0 if (shouldResetBackoff(hadData, sessionUptime, SHORT_SESSION_THRESHOLD_MS)) { - Logger.d { "$logTag: [$address] Resetting backoff after successful data exchange (${sessionUptime}ms)" } + Logger.d { + "$logTag: [${address.anonymizePublicHost()}] Resetting backoff after successful data exchange " + + "(${sessionUptime}ms)" + } retryCount = 1 backoff = MIN_BACKOFF_MILLIS } else if (hadData) { - val backoffSec = backoff / MILLIS_PER_SECOND + val backoffSec = backoff / TimeConstants.MS_PER_SEC Logger.d { - "$logTag: [$address] Short session (${sessionUptime}ms) — keeping backoff at ${backoffSec}s" + "$logTag: [${address.anonymizePublicHost()}] Short session (${sessionUptime}ms); " + + "keeping backoff at ${backoffSec}s" } } - val delaySec = backoff / MILLIS_PER_SECOND - Logger.i { "$logTag: [$address] Reconnect #$retryCount in ${delaySec}s" } + val delaySec = backoff / TimeConstants.MS_PER_SEC + Logger.i { "$logTag: [${address.anonymizePublicHost()}] Reconnect #$retryCount in ${delaySec}s" } delay(backoff) retryCount++ backoff = minOf(backoff * 2, MAX_BACKOFF_MILLIS) @@ -264,7 +270,7 @@ class TcpTransport( private suspend fun connectAndRead(address: String): Boolean = withContext(dispatchers.io) { val (host, port) = parseHostAndPort(address) - Logger.i { "$logTag: [$address] Connecting to $host:$port" } + Logger.i { "$logTag: [${address.anonymizePublicHost()}] Connecting to ${host.anonymizePublicHost()}:$port" } val attemptStart = nowMillis val selector = SelectorManager(dispatchers.io) @@ -284,7 +290,7 @@ class TcpTransport( resetMetrics() codec.reset() - Logger.i { "$logTag: [$address] Socket connected in ${connectTime}ms" } + Logger.i { "$logTag: [${address.anonymizePublicHost()}] Socket connected in ${connectTime}ms" } val output = sock.openWriteChannel(autoFlush = false) writeChannel = output @@ -342,12 +348,12 @@ class TcpTransport( timeoutCount++ timeoutEvents++ if (timeoutCount % TIMEOUT_LOG_INTERVAL == 0) { - Logger.d { "$logTag: [$address] Timeout $timeoutCount/$SOCKET_RETRIES" } + Logger.d { "$logTag: [${address.anonymizePublicHost()}] Timeout $timeoutCount/$SOCKET_RETRIES" } } } read == -1 -> { - Logger.i { "$logTag: [$address] EOF after $packetsReceived packets" } + Logger.i { "$logTag: [${address.anonymizePublicHost()}] EOF after $packetsReceived packets" } return } @@ -360,7 +366,7 @@ class TcpTransport( } } } - Logger.w { "$logTag: [$address] Closing after $SOCKET_RETRIES consecutive timeouts" } + Logger.w { "$logTag: [${address.anonymizePublicHost()}] Closing after $SOCKET_RETRIES consecutive timeouts" } } // Guards against recursive disconnects triggered by listener callbacks. @@ -375,14 +381,14 @@ class TcpTransport( if (s != null) { val uptime = if (connectionStartTime > 0) nowMillis - connectionStartTime else 0 Logger.i { - "$logTag: [$currentAddress] Disconnecting - Uptime: ${uptime}ms, " + + "$logTag: [${currentAddress?.anonymizePublicHost()}] Disconnecting - Uptime: ${uptime}ms, " + "RX: $packetsReceived ($bytesReceived bytes), " + "TX: $packetsSent ($bytesSent bytes)" } try { s.close() } catch (ex: IOException) { - Logger.w(ex) { "$logTag: [$currentAddress] Error closing socket" } + Logger.w(ex) { "$logTag: [${currentAddress?.anonymizePublicHost()}] Error closing socket" } } } selectorManager?.close() @@ -408,13 +414,15 @@ class TcpTransport( val stream = writeChannel ?: run { - Logger.w { "$logTag: [$currentAddress] Cannot send ${p.size} bytes: not connected" } + Logger.w { + "$logTag: [${currentAddress?.anonymizePublicHost()}] Cannot send ${p.size} bytes: not connected" + } return } try { stream.writeFully(p) } catch (ex: IOException) { - Logger.w(ex) { "$logTag: [$currentAddress] TCP write error" } + Logger.w(ex) { "$logTag: [${currentAddress?.anonymizePublicHost()}] TCP write error" } disconnectSocket() } } @@ -424,7 +432,7 @@ class TcpTransport( try { stream.flush() } catch (ex: IOException) { - Logger.w(ex) { "$logTag: [$currentAddress] TCP flush error" } + Logger.w(ex) { "$logTag: [${currentAddress?.anonymizePublicHost()}] TCP flush error" } disconnectSocket() } } diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleAddressAdmissionTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleAddressAdmissionTest.kt new file mode 100644 index 0000000000..cd753498b5 --- /dev/null +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleAddressAdmissionTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.network.radio + +import dev.mokkery.MockMode +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioTransport +import org.meshtastic.core.testing.FakeBluetoothRepository +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * A saved BLE address restored onto hardware with no Bluetooth LE must be refused, so the radio service falls into its + * "No valid address" no-op instead of arming a BLE transport that retries forever. + */ +class BleAddressAdmissionTest { + + private val bluetoothRepository = FakeBluetoothRepository() + + private fun factory() = object : + BaseRadioTransportFactory( + scanner = mock(MockMode.autofill), + bluetoothRepository = bluetoothRepository, + connectionFactory = mock(MockMode.autofill), + dispatchers = + CoroutineDispatchers( + io = Dispatchers.Unconfined, + main = Dispatchers.Unconfined, + default = Dispatchers.Unconfined, + ), + ) { + override val supportedDeviceTypes: List = listOf(DeviceType.TCP) + + override val mockTransportEnabled: StateFlow = MutableStateFlow(false) + + override val isReplayTransportAvailable: Boolean = false + + override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport = + NopRadioTransport(address) + } + + @Test + fun `BLE addresses are refused on hardware without Bluetooth LE`() { + bluetoothRepository.isSupported = false + val factory = factory() + + assertFalse(factory.isAddressValid("x11:22:33:44:55:66")) + assertFalse(factory.isAddressValid("!11:22:33:44:55:66")) + } + + @Test + fun `other transports stay valid on hardware without Bluetooth LE`() { + bluetoothRepository.isSupported = false + val factory = factory() + + assertTrue(factory.isAddressValid("t10.0.0.2")) + assertTrue(factory.isAddressValid("s/dev/ttyUSB0")) + } + + @Test + fun `BLE addresses are admitted when Bluetooth LE is present`() { + val factory = factory() + + assertTrue(factory.isAddressValid("x11:22:33:44:55:66")) + assertTrue(factory.isAddressValid("!11:22:33:44:55:66")) + } +} diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt index 7cfe56cba5..5303d931e9 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt @@ -44,6 +44,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.model.RadioNotConnectedException import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.testing.CapturingLogWriter import org.meshtastic.core.testing.FakeBleConnection import org.meshtastic.core.testing.FakeBleConnectionFactory import org.meshtastic.core.testing.FakeBleDevice @@ -195,6 +196,29 @@ class BleRadioTransportTest { } } + @Test + fun `a retried BLE write failure logs the device address only in anonymized form`() = runTest { + val device = FakeBleDevice(address = address, name = "Test Device") + bluetoothRepository.bond(device) + scanner.emitDevice(device) + val logs = CapturingLogWriter.install() + val bleTransport = bleTransportOn(this, FakeRadioInterfaceService()) + bleTransport.start() + + try { + advanceTimeBy(4_000L) + connection.service.writeException = RuntimeException("write rejected") + assertTrue(bleTransport.handleSendToRadio(byteArrayOf(1, 2, 3))) + advanceTimeBy(1_000L) + + assertTrue(logs.messages().any { "BLE operation failed" in it }, "the write retry path must have logged") + logs.assertNotLogged(address) + } finally { + bleTransport.close() + CapturingLogWriter.uninstall() + } + } + @Test fun `close drains every write admitted by the active BLE profile generation`() = runTest { val device = FakeBleDevice(address = address, name = "Test Device") diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt index 1e39ee8777..7c195417d2 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt @@ -23,8 +23,10 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString +import org.meshtastic.core.common.util.crc32 import org.meshtastic.core.repository.HandshakeConstants import org.meshtastic.core.repository.RadioTransportCallback import org.meshtastic.core.repository.TransportDisconnectReason @@ -509,9 +511,101 @@ class MockRadioTransportTest { } } + @Test + fun `the showcase address plays the showcase mesh under its own identity`() = runTest { + val callback = RecordingCallback() + val scope = transportScope() + try { + val transport = MockRadioTransport(callback, scope, address = MockScenario.SHOWCASE_ADDRESS) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) + + val myInfo = assertNotNull(callback.received.firstNotNullOfOrNull { it.my_info }) + assertEquals(MockScenario.SHOWCASE.myNode, myInfo.my_node_num) + val names = callback.nodeInfos.map { it.user?.long_name } + assertEquals(listOf("Base Camp") + MockScenario.SHOWCASE.peers.map { it.longName }, names) + assertFalse(names.contains("Demo Handset"), "the showcase must not carry Demo Mode's identity") + val peerKeys = callback.nodeInfos.drop(1).map { it.user?.public_key?.size } + assertEquals(List(MockScenario.SHOWCASE.peers.size) { PUBLIC_KEY_SIZE }, peerKeys) + } finally { + scope.cancel() + } + } + + @Test + fun `every showcase peer number is the crc32 of its public key`() { + MockScenario.SHOWCASE.peers.forEach { peer -> + val key = assertNotNull(peer.publicKey, "${peer.longName} has no public key").decodeHex() + assertEquals(peer.num, key.crc32().toInt(), "${peer.longName} does not carry its key's node number") + } + } + + @Test + fun `showcase battery voltages sit on the firmware LiPo curve`() { + MockScenario.SHOWCASE.peers + .filter { it.batteryLevel <= MAX_BATTERY_PERCENT } + .forEach { peer -> + val voltage = assertNotNull(peer.voltage, "${peer.longName} reports a charge level without a voltage") + assertEquals(lipoVolts(peer.batteryLevel), voltage, VOLTAGE_TOLERANCE, peer.longName) + } + } + + @Test + fun `any other address plays the demo mesh`() = runTest { + val callback = RecordingCallback() + val scope = transportScope() + try { + val transport = MockRadioTransport(callback, scope, address = "anything") + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) + + assertEquals(MockScenario.DEMO.myNode, callback.received.firstNotNullOfOrNull { it.my_info }?.my_node_num) + assertEquals("Demo Handset", callback.nodeInfos.first().user?.long_name) + } finally { + scope.cancel() + } + } + + @Test + fun `the showcase seeds its thread once and then stays still`() = runTest { + val callback = RecordingCallback() + val scope = transportScope() + try { + val transport = MockRadioTransport(callback, scope, address = MockScenario.SHOWCASE_ADDRESS) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) + testScheduler.advanceTimeBy(SEED_WINDOW_MS) + + val texts = callback.packetsOn(PortNum.TEXT_MESSAGE_APP) + assertEquals( + MockScenario.SHOWCASE.channelConversation.size + MockScenario.SHOWCASE.directConversation.size, + texts.size, + ) + val afterSeed = callback.received.size + + testScheduler.advanceTimeBy(LIVE_TICK_MS * LIVE_TICKS_AFTER_CLOSE) + assertEquals(afterSeed, callback.received.size, "a capture must see the same numbers however late it runs") + } finally { + scope.cancel() + } + } + private companion object { const val BROADCAST_ADDR = -1 const val MIN_DEMO_NODES = 8 + const val PUBLIC_KEY_SIZE = 32 + const val MAX_BATTERY_PERCENT = 100 + const val VOLTAGE_TOLERANCE = 0.01f + + /** Firmware's `OCV_ARRAY` for a LiPo cell (`Power.h`): millivolts at 100%, 90%, ... 0%. */ + val LIPO_OCV_MV = intArrayOf(4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100) + + fun lipoVolts(percent: Int): Float { + val band = (MAX_BATTERY_PERCENT - percent) / 10 + val upper = LIPO_OCV_MV[band] + val lower = LIPO_OCV_MV[minOf(band + 1, LIPO_OCV_MV.lastIndex)] + return (lower + (upper - lower) * (percent % 10) / 10f) / 1000f + } /** Comfortably longer than the seed pass, but shorter than the first live-telemetry tick. */ const val SEED_WINDOW_MS = 10_000L diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockTransportAddressAdmissionTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockTransportAddressAdmissionTest.kt index d1fd62f889..987e682d21 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockTransportAddressAdmissionTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockTransportAddressAdmissionTest.kt @@ -23,11 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner -import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceType import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioTransport +import org.meshtastic.core.testing.FakeBluetoothRepository import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -46,7 +46,7 @@ class MockTransportAddressAdmissionTest { object : BaseRadioTransportFactory( scanner = mock(MockMode.autofill), - bluetoothRepository = mock(MockMode.autofill), + bluetoothRepository = FakeBluetoothRepository(), connectionFactory = mock(MockMode.autofill), dispatchers = CoroutineDispatchers( diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt index b37e985373..17a3c5b46d 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt @@ -55,7 +55,6 @@ import kotlin.random.Random * the full `MeshMessageProcessor` graph and is a separate integration harness. The [adversarialFromRadio] corpus here * is designed to feed that harness when it lands. */ -@Suppress("MagicNumber") object ReplayFuzz { /** Default seed sweep per invariant. Each seed is microseconds of work, so this stays well within CI budgets. */ diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/SerialAddressAdmissionTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/SerialAddressAdmissionTest.kt new file mode 100644 index 0000000000..968083dd93 --- /dev/null +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/SerialAddressAdmissionTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.network.radio + +import dev.mokkery.MockMode +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioTransport +import org.meshtastic.core.testing.FakeBluetoothRepository +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * A saved serial address restored onto hardware with no USB host must be refused, so the radio service falls into its + * "No valid address" no-op instead of building a serial transport that can never find its device. + */ +class SerialAddressAdmissionTest { + + private fun factory(serialSupported: Boolean) = object : + BaseRadioTransportFactory( + scanner = mock(MockMode.autofill), + bluetoothRepository = FakeBluetoothRepository(), + connectionFactory = mock(MockMode.autofill), + dispatchers = + CoroutineDispatchers( + io = Dispatchers.Unconfined, + main = Dispatchers.Unconfined, + default = Dispatchers.Unconfined, + ), + ) { + override val supportedDeviceTypes: List = listOf(DeviceType.TCP) + + override val mockTransportEnabled: StateFlow = MutableStateFlow(false) + + override val isReplayTransportAvailable: Boolean = false + + override val isSerialSupported: Boolean = serialSupported + + override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport = + NopRadioTransport(address) + } + + @Test + fun `serial addresses are refused on hardware without USB host`() { + assertFalse(factory(serialSupported = false).isAddressValid("s1027:29987:0")) + } + + @Test + fun `other transports stay valid on hardware without USB host`() { + val factory = factory(serialSupported = false) + + assertTrue(factory.isAddressValid("t10.0.0.2")) + assertTrue(factory.isAddressValid("x11:22:33:44:55:66")) + } + + @Test + fun `serial addresses are admitted with USB host whether or not the device is present`() { + assertTrue(factory(serialSupported = true).isAddressValid("s1027:29987:0")) + } +} diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt index 500c2e6b16..fcd4688beb 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt @@ -16,26 +16,18 @@ */ package org.meshtastic.core.network.repository -import co.touchlab.kermit.LogWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import co.touchlab.kermit.loggerConfigInit import kotlinx.io.IOException +import org.meshtastic.core.testing.CapturingLogWriter import org.meshtastic.mqtt.MqttLogLevel import kotlin.test.Test import kotlin.test.assertEquals class KermitMqttLoggerTest { - private class CapturingWriter : LogWriter() { - val entries = mutableListOf>() - - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - entries += Triple(severity, tag, message) - } - } - - private val writer = CapturingWriter() + private val writer = CapturingLogWriter() private val mqttLogger = KermitMqttLogger(Logger(loggerConfigInit(writer), tag = "Test")) @Test @@ -50,8 +42,8 @@ class KermitMqttLoggerTest { ) assertEquals(1, writer.entries.size) - assertEquals(Severity.Warn, writer.entries[0].first) - assertEquals("MqttConnection", writer.entries[0].second) + assertEquals(Severity.Warn, writer.entries[0].severity) + assertEquals("MqttConnection", writer.entries[0].tag) } @Test @@ -63,7 +55,7 @@ class KermitMqttLoggerTest { throwable = IllegalStateException("bad state"), ) - assertEquals(Severity.Error, writer.entries.single().first) + assertEquals(Severity.Error, writer.entries.single().severity) } @Test @@ -71,7 +63,7 @@ class KermitMqttLoggerTest { // A bare message carries no stack or type to triage, so it must not count as an application error. mqttLogger.log(level = MqttLogLevel.ERROR, tag = "MqttClient", message = "boom", throwable = null) - assertEquals(Severity.Warn, writer.entries.single().first) + assertEquals(Severity.Warn, writer.entries.single().severity) } @Test @@ -83,7 +75,7 @@ class KermitMqttLoggerTest { throwable = null, ) - assertEquals(Severity.Warn, writer.entries.single().first) + assertEquals(Severity.Warn, writer.entries.single().severity) } @Test @@ -96,7 +88,7 @@ class KermitMqttLoggerTest { throwable = null, ) - assertEquals(Severity.Warn, writer.entries.single().first) + assertEquals(Severity.Warn, writer.entries.single().severity) } @Test @@ -105,8 +97,8 @@ class KermitMqttLoggerTest { mqttLogger.log(MqttLogLevel.INFO, "MqttClient", "info", null) mqttLogger.log(MqttLogLevel.DEBUG, "MqttClient", "debug", null) - assertEquals(listOf(Severity.Warn, Severity.Info, Severity.Debug), writer.entries.map { it.first }) - assertEquals(listOf("MqttClient", "MqttClient", "MqttClient"), writer.entries.map { it.second }) + assertEquals(listOf(Severity.Warn, Severity.Info, Severity.Debug), writer.entries.map { it.severity }) + assertEquals(listOf("MqttClient", "MqttClient", "MqttClient"), writer.entries.map { it.tag }) } @Test diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt index 4de981d3de..015f33b014 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt @@ -47,6 +47,7 @@ import org.meshtastic.mqtt.MqttEndpoint import org.meshtastic.mqtt.MqttException import org.meshtastic.mqtt.MqttLogLevel import org.meshtastic.mqtt.MqttMessage +import org.meshtastic.mqtt.MqttProtocolVersion import org.meshtastic.mqtt.QoS import org.meshtastic.mqtt.ReasonCode import org.meshtastic.mqtt.packet.Subscription @@ -64,6 +65,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) @@ -514,6 +516,56 @@ class MQTTRepositoryImplTest { runCurrent() } + @Test + fun `a refused topic filter is recorded for the UI without restarting the connection`() = runTest { + val harness = createHarness() + val refusal = + MqttException.SubscriptionRefused( + reasonCode = ReasonCode.NOT_AUTHORIZED, + message = "The broker refused 'msh/2/e/alpha/+' (NOT_AUTHORIZED)", + refused = mapOf("msh/2/e/alpha/+" to ReasonCode.NOT_AUTHORIZED), + granted = listOf("msh/2/e/PKI/+"), + ) + harness.client.failSubscribeWith(refusal) + + val collector = startProxyCollection(harness.repository) + runCurrent() + assertEquals(1, harness.client.connectCalls.size) + assertEquals(1, harness.client.subscribeCalls.size) + assertSame(refusal, harness.repository.subscriptionRefusal.value) + + advanceTimeBy(60_000) + runCurrent() + assertEquals(1, harness.client.connectCalls.size) + assertEquals(1, harness.client.subscribeCalls.size) + + collector.cancelAndJoin() + runCurrent() + assertNull(harness.repository.subscriptionRefusal.value) + } + + @Test + fun `a malformed SUBACK fails the attempt and the connect loop retries it`() = runTest { + val harness = createHarness() + harness.client.failSubscribeWith( + MqttException.ProtocolError(ReasonCode.PROTOCOL_ERROR, "SUBACK carried 1 reason codes for 2 topic filters"), + ) + + val collector = startProxyCollection(harness.repository) + runCurrent() + assertEquals(1, harness.client.connectCalls.size) + assertEquals(1, harness.client.subscribeCalls.size) + assertNull(harness.repository.subscriptionRefusal.value) + + advanceTimeBy(1_000) + runCurrent() + assertEquals(2, harness.client.connectCalls.size) + assertEquals(2, harness.client.subscribeCalls.size) + + collector.cancelAndJoin() + runCurrent() + } + @Test fun `connection state flow reflects active client state updates`() = runTest { val harness = createHarness() @@ -710,6 +762,13 @@ class MQTTRepositoryImplTest { // region isUndeliverableDownlink — Tier 1 drop filter for MQTT client-proxy downlink packets. + private val usNum = 0x12345678 + private val usId = "!12345678" + + /** An envelope with both halves of the self-traffic decision set: who sent it, and who gatewayed it. */ + private fun selfEnvelope(from: Int, gatewayId: String): ByteArray = + envelopeBytes(gatewayId = gatewayId, packet = MeshPacket.Builder().also { wb -> wb.from = from }.build()) + private fun envelopeBytes( channelId: String = "LongFast", gatewayId: String = "!aabbccdd", @@ -824,6 +883,98 @@ class MQTTRepositoryImplTest { assertContentEquals(real, proxyMessage.data_?.toByteArray()) } + @Test + fun `a 3 1 1 broker is subscribed without the MQTT 5 noLocal option`() = runTest { + val client = FakeMqttClientSession().apply { negotiatedProtocolVersion = MqttProtocolVersion.V3_1_1 } + val harness = createHarness(client = client) + + val collector = startProxyCollection(harness.repository) + runCurrent() + + val subscriptions = harness.client.subscribeCalls.single() + assertTrue(subscriptions.isNotEmpty(), "a 3.1.1 broker must still be subscribed") + assertTrue( + subscriptions.none { it.noLocal }, + "noLocal is an MQTT 5 option and makes the client reject the whole SUBSCRIBE on 3.1.1", + ) + + collector.cancel() + } + + @Test + fun `a 5 0 broker keeps noLocal so the broker does not echo our own uplink`() = runTest { + val client = FakeMqttClientSession().apply { negotiatedProtocolVersion = MqttProtocolVersion.V5_0 } + val harness = createHarness(client = client) + + val collector = startProxyCollection(harness.repository) + runCurrent() + + val subscriptions = harness.client.subscribeCalls.single() + assertTrue(subscriptions.isNotEmpty()) + assertTrue(subscriptions.all { it.noLocal }) + + collector.cancel() + } + + @Test + fun `our own uplink returning via our own gateway is forwarded as firmware reads it as a local ack`() { + val ours = selfEnvelope(from = usNum, gatewayId = usId) + + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttSelfTraffic(ours, myId = usId)) + } + + @Test + fun `our node id arriving via someone else's gateway is a forgery and is dropped`() { + val forged = selfEnvelope(from = usNum, gatewayId = "!aabbccdd") + + assertEquals(MqttSelfTraffic.FORGED_SENDER, classifyMqttSelfTraffic(forged, myId = usId)) + } + + @Test + fun `our own uplink of someone else's packet coming back is dropped`() { + val bridged = selfEnvelope(from = 0xAABBCCDD.toInt(), gatewayId = usId) + + assertEquals(MqttSelfTraffic.OWN_UPLINK_ECHO, classifyMqttSelfTraffic(bridged, myId = usId)) + } + + @Test + fun `an unrelated packet from an unrelated gateway is forwarded`() { + val theirs = selfEnvelope(from = 0xAABBCCDD.toInt(), gatewayId = "!99887766") + + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttSelfTraffic(theirs, myId = usId)) + } + + @Test + fun `self-traffic classification fails open before the local node id is known`() { + val ours = selfEnvelope(from = usNum, gatewayId = "!aabbccdd") + + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttSelfTraffic(ours, myId = null)) + } + + @Test + fun `self-traffic classification fails open on unparseable bytes`() { + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttSelfTraffic(byteArrayOf(-1, -1, -1, -1), myId = usId)) + } + + @Test + fun `the json topic applies the same three-way rule`() { + val us = usNum.toLong() and 0xFFFFFFFFL + val them = 0xAABBCCDDL + + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttJsonSelfTraffic(us, usId, usId)) + assertEquals(MqttSelfTraffic.FORGED_SENDER, classifyMqttJsonSelfTraffic(us, "!aabbccdd", usId)) + assertEquals(MqttSelfTraffic.OWN_UPLINK_ECHO, classifyMqttJsonSelfTraffic(them, usId, usId)) + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttJsonSelfTraffic(them, "!99887766", usId)) + } + + @Test + fun `the json topic fails open when the sender or local id is unknown`() { + val us = usNum.toLong() and 0xFFFFFFFFL + + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttJsonSelfTraffic(us, null, usId)) + assertEquals(MqttSelfTraffic.FORWARD, classifyMqttJsonSelfTraffic(us, usId, null)) + } + // endregion private fun TestScope.createHarness( @@ -879,6 +1030,7 @@ class MQTTRepositoryImplTest { private val mutableMessages = MutableSharedFlow(extraBufferCapacity = 8) override val messages: Flow = mutableMessages override val connectionState = MutableStateFlow(ConnectionState.Disconnected.Idle) + override var negotiatedProtocolVersion: MqttProtocolVersion = MqttProtocolVersion.V5_0 val connectCalls = mutableListOf() val subscribeCalls = mutableListOf>() val publishStarted = mutableListOf() diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/transport/TcpTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/transport/TcpTransportTest.kt index 215b1c2581..d9945a8d0c 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/transport/TcpTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/transport/TcpTransportTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.core.network.transport import io.ktor.network.selector.SelectorManager diff --git a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt index d2fc834380..8fb841bfa5 100644 --- a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt +++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt @@ -301,6 +301,8 @@ private constructor( } /** Attempts to open the serial port and starts the read loop. Returns true if successful, false otherwise. */ + // Cancellation is inspected after runCatching: the port is closed, then the cancellation rethrown. + @Suppress("SuspendFunSwallowedCancellation") private suspend fun startConnection(): Boolean { if (portState.isClosing(lifecycle.isClosed)) return false var candidatePort: SerialPort? = null @@ -429,12 +431,11 @@ private constructor( // beginClose makes new sends reject synchronously. Stop the framed-send worker now so queued sends release // their lifecycle leases before the gate waits for admitted operations. super.close() - val completed = - lifecycle.close { - val currentReadJob = portState.takeReadJob() - runInterruptible(dispatchers.io) { portState.takePort()?.let { closeSerialPort(portName, it) } } - currentReadJob?.cancelAndJoin() - } + val completed = lifecycle.close { + val currentReadJob = portState.takeReadJob() + runInterruptible(dispatchers.io) { portState.takePort()?.let { closeSerialPort(portName, it) } } + currentReadJob?.cancelAndJoin() + } if (!completed) Logger.w { "[$portName] JVM serial teardown did not complete within its lifecycle bounds" } } } diff --git a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt index 8eb0e01837..dd7bbfba8c 100644 --- a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt +++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.di.CoroutineDispatchers import java.io.IOException import java.net.InetAddress @@ -38,7 +39,8 @@ class JvmServiceDiscovery(private val dispatchers: CoroutineDispatchers) : Servi trySend(emptyList()) // Emit initial empty list so downstream combine() is not blocked val bindAddress = findLanAddress() ?: InetAddress.getLocalHost() - Logger.i { "JmDNS binding to ${bindAddress.hostAddress}" } + val interfaceName = safeCatching { NetworkInterface.getByInetAddress(bindAddress)?.name }.getOrNull() + Logger.i { "JmDNS binding to interface ${interfaceName ?: "unknown"}" } val jmdns = try { diff --git a/core/nfc/README.md b/core/nfc/README.md index ce98040689..18952e0a66 100644 --- a/core/nfc/README.md +++ b/core/nfc/README.md @@ -1,12 +1,12 @@ # `:core:nfc` ## Overview -The `:core:nfc` module provides Near Field Communication (NFC) capabilities for the application. It is a KMP module with Android NFC hardware implementation isolated to `androidMain`. The shared NFC contract is provided via `LocalNfcScannerProvider` in `core:ui`. +The `:core:nfc` module provides Near Field Communication (NFC) capabilities for the application. It is an Android library, since NFC hardware APIs are Android-specific. The shared NFC contract is provided via `LocalNfcScannerProvider` in `core:ui`. ## Key Components -### 1. `NfcScannerEffect` (androidMain) -A Composable side-effect that manages Android NFC adapter state and listens for NDEF tags. Located in `androidMain` since NFC hardware APIs are Android-specific. +### 1. `NfcScannerEffect` +A Composable side-effect that manages Android NFC adapter state and listens for NDEF tags. ### 2. `LocalNfcScannerProvider` (core:ui/commonMain) The shared capability contract for NFC scanning, injected via `CompositionLocalProvider` from the app layer. @@ -17,7 +17,7 @@ The shared capability contract for NFC scanning, injected via `CompositionLocalP ```mermaid graph TB - :core:nfc[nfc]:::kmp-library-compose + :core:nfc[nfc]:::android-library classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts index 935eec87b1..c8783bda11 100644 --- a/core/nfc/build.gradle.kts +++ b/core/nfc/build.gradle.kts @@ -14,19 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import com.android.build.api.dsl.LibraryExtension plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.android.library) + alias(libs.plugins.meshtastic.android.library.compose) } -kotlin { - sourceSets { - commonMain.dependencies { implementation(libs.kermit) } +configure { namespace = "org.meshtastic.core.nfc" } - androidMain.dependencies { - implementation(libs.androidx.activity.compose) - implementation(libs.compose.multiplatform.ui) - } - } +dependencies { + implementation(libs.androidx.activity.compose) + implementation(libs.compose.multiplatform.ui) + implementation(libs.kermit) } diff --git a/core/nfc/detekt-baseline.xml b/core/nfc/detekt-baseline.xml index f766876e59..4406f4f228 100644 --- a/core/nfc/detekt-baseline.xml +++ b/core/nfc/detekt-baseline.xml @@ -2,7 +2,6 @@ - LambdaParameterInRestartableEffect:NfcScanner.kt:onResult: (String?) -> Unit ParameterNaming:NfcScanner.kt:onNfcDisabled: (() -> Unit)? = null diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt b/core/nfc/src/main/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt similarity index 100% rename from core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt rename to core/nfc/src/main/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt b/core/nfc/src/main/kotlin/org/meshtastic/core/nfc/NfcScanner.kt similarity index 97% rename from core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt rename to core/nfc/src/main/kotlin/org/meshtastic/core/nfc/NfcScanner.kt index b8fb2434b6..a3dab33e56 100644 --- a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt +++ b/core/nfc/src/main/kotlin/org/meshtastic/core/nfc/NfcScanner.kt @@ -40,15 +40,17 @@ fun NfcScannerEffect(onResult: (String?) -> Unit, onNfcDisabled: (() -> Unit)? = val activity = context as? Activity ?: return val nfcAdapter = remember { NfcAdapter.getDefaultAdapter(context) } + val currentOnResult by rememberUpdatedState(onResult) + val currentOnNfcDisabled by rememberUpdatedState(onNfcDisabled) DisposableEffect(nfcAdapter) { if (nfcAdapter == null) { onDispose {} } else if (!nfcAdapter.isEnabled) { - onNfcDisabled?.invoke() + currentOnNfcDisabled?.invoke() onDispose {} } else { - val readerCallback = NfcAdapter.ReaderCallback { tag: Tag -> handleNfcTag(tag, onResult) } + val readerCallback = NfcAdapter.ReaderCallback { tag: Tag -> handleNfcTag(tag) { currentOnResult(it) } } val flags = ( diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt b/core/nfc/src/main/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt similarity index 100% rename from core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt rename to core/nfc/src/main/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt diff --git a/core/prefs/detekt-baseline.xml b/core/prefs/detekt-baseline.xml new file mode 100644 index 0000000000..d28a03b270 --- /dev/null +++ b/core/prefs/detekt-baseline.xml @@ -0,0 +1,8 @@ + + + + + UseOrEmpty:NotificationPrefsImpl.kt:NotificationPrefsImpl.Companion$csv?.split(',')?.mapNotNull { it.toIntOrNull() }?.distinct() ?: emptyList() + UseOrEmpty:UiPrefsImpl.kt:UiPrefsImpl$preferences[KEY_FIRMWARE_UPDATE_NOTIFICATION_KEYS]?.split('|')?.filter(String::isNotBlank)?.toSet() ?: emptySet() + + diff --git a/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/di/CreatePreferencesDataStoreTest.kt b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/di/CreatePreferencesDataStoreTest.kt new file mode 100644 index 0000000000..a179b73e23 --- /dev/null +++ b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/di/CreatePreferencesDataStoreTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.di + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.meshtastic.core.di.CoroutineDispatchers +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class CreatePreferencesDataStoreTest { + + @Test + fun `a corrupt preferences file reads as empty and accepts new writes`() = runTest { + val context = ApplicationProvider.getApplicationContext() + val file = context.preferencesDataStoreFile(FILE_NAME) + file.parentFile?.mkdirs() + // A varint whose continuation bit never clears cannot parse as a protobuf. + file.writeBytes(ByteArray(GARBAGE_LENGTH) { 0xFF.toByte() }) + + val dispatcher = StandardTestDispatcher(testScheduler) + val store = + createPreferencesDataStore( + context, + CoroutineDispatchers(io = dispatcher, main = dispatcher, default = dispatcher), + legacyName = "corrupt-prefs", + fileName = FILE_NAME, + ) + + assertTrue(store.data.first().asMap().isEmpty()) + + store.edit { it[KEY] = true } + assertEquals(true, store.data.first()[KEY]) + } + + private companion object { + const val FILE_NAME = "corrupt_ds" + const val GARBAGE_LENGTH = 16 + val KEY = booleanPreferencesKey("written-after-recovery") + } +} diff --git a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt index 5ff3af23cb..2e6a533011 100644 --- a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt +++ b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt @@ -18,9 +18,11 @@ package org.meshtastic.core.prefs.di import android.content.Context import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -40,70 +42,95 @@ class CorePrefsAndroidModule { @Single fun provideAnalyticsDataStore(context: Context, dispatchers: CoroutineDispatchers): AnalyticsDataStore = - store(context, dispatchers, legacyName = "analytics-prefs", fileName = "analytics_ds").asAnalyticsDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "analytics-prefs", fileName = "analytics_ds") + .asAnalyticsDataStore() @Single fun provideHomoglyphEncodingDataStore( context: Context, dispatchers: CoroutineDispatchers, - ): HomoglyphEncodingDataStore = - store(context, dispatchers, legacyName = "homoglyph-encoding-prefs", fileName = "homoglyph_encoding_ds") - .asHomoglyphEncodingDataStore() + ): HomoglyphEncodingDataStore = createPreferencesDataStore( + context, + dispatchers, + legacyName = "homoglyph-encoding-prefs", + fileName = "homoglyph_encoding_ds", + ) + .asHomoglyphEncodingDataStore() @Single fun provideAppDataStore(context: Context, dispatchers: CoroutineDispatchers): AppDataStore = - store(context, dispatchers, legacyName = "prefs", fileName = "app_ds").asAppDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "prefs", fileName = "app_ds").asAppDataStore() @Single fun provideCustomEmojiDataStore(context: Context, dispatchers: CoroutineDispatchers): CustomEmojiDataStore = - store(context, dispatchers, legacyName = "org.geeksville.emoji.prefs", fileName = "custom_emoji_ds") + createPreferencesDataStore( + context, + dispatchers, + legacyName = "org.geeksville.emoji.prefs", + fileName = "custom_emoji_ds", + ) .asCustomEmojiDataStore() @Single fun provideMapDataStore(context: Context, dispatchers: CoroutineDispatchers): MapDataStore = - store(context, dispatchers, legacyName = "map_prefs", fileName = "map_ds").asMapDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "map_prefs", fileName = "map_ds").asMapDataStore() @Single fun provideMapConsentDataStore(context: Context, dispatchers: CoroutineDispatchers): MapConsentDataStore = - store(context, dispatchers, legacyName = "map_consent_preferences", fileName = "map_consent_ds") + createPreferencesDataStore( + context, + dispatchers, + legacyName = "map_consent_preferences", + fileName = "map_consent_ds", + ) .asMapConsentDataStore() @Single fun provideMapTileProviderDataStore(context: Context, dispatchers: CoroutineDispatchers): MapTileProviderDataStore = - store(context, dispatchers, legacyName = "map_tile_provider_prefs", fileName = "map_tile_provider_ds") + createPreferencesDataStore( + context, + dispatchers, + legacyName = "map_tile_provider_prefs", + fileName = "map_tile_provider_ds", + ) .asMapTileProviderDataStore() @Single fun provideMeshDataStore(context: Context, dispatchers: CoroutineDispatchers): MeshDataStore = - store(context, dispatchers, legacyName = "mesh-prefs", fileName = "mesh_ds").asMeshDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "mesh-prefs", fileName = "mesh_ds") + .asMeshDataStore() @Single fun provideRadioDataStore(context: Context, dispatchers: CoroutineDispatchers): RadioDataStore = - store(context, dispatchers, legacyName = "radio-prefs", fileName = "radio_ds").asRadioDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "radio-prefs", fileName = "radio_ds") + .asRadioDataStore() @Single fun provideUiDataStore(context: Context, dispatchers: CoroutineDispatchers): UiDataStore = - store(context, dispatchers, legacyName = "ui-prefs", fileName = "ui_ds").asUiDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "ui-prefs", fileName = "ui_ds").asUiDataStore() @Single fun provideMeshLogDataStore(context: Context, dispatchers: CoroutineDispatchers): MeshLogDataStore = - store(context, dispatchers, legacyName = "meshlog-prefs", fileName = "meshlog_ds").asMeshLogDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "meshlog-prefs", fileName = "meshlog_ds") + .asMeshLogDataStore() @Single fun provideFilterDataStore(context: Context, dispatchers: CoroutineDispatchers): FilterDataStore = - store(context, dispatchers, legacyName = "filter-prefs", fileName = "filter_ds").asFilterDataStore() + createPreferencesDataStore(context, dispatchers, legacyName = "filter-prefs", fileName = "filter_ds") + .asFilterDataStore() } /** * [legacyName] is the SharedPreferences file this domain migrates from, [fileName] the DataStore file it lives in now. * Both are on-disk identities — changing either orphans existing user data. */ -private fun store( +fun createPreferencesDataStore( context: Context, dispatchers: CoroutineDispatchers, legacyName: String, fileName: String, ): DataStore = PreferenceDataStoreFactory.create( + corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }), migrations = listOf(SharedPreferencesMigration(context, legacyName)), scope = CoroutineScope(dispatchers.io + SupervisorJob()), produceFile = { context.preferencesDataStoreFile(fileName) }, diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt index a55e3d46c6..f012328a11 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt @@ -41,13 +41,19 @@ class AnalyticsPrefsImpl( ) : AnalyticsPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + // Consent is not granted until the stored choice has been read, so a decision made before then never sees + // ALLOWED_BY_DEFAULT in place of an opt-out. override val analyticsAllowed: StateFlow = analyticsDataStore.data - .map { it[KEY_ANALYTICS_ALLOWED_PREF] ?: true } - .stateIn(scope, SharingStarted.Eagerly, true) + .map { it[KEY_ANALYTICS_ALLOWED_PREF] ?: ALLOWED_BY_DEFAULT } + .stateIn(scope, SharingStarted.Eagerly, ALLOWED_BEFORE_LOAD) - override fun setAnalyticsAllowed(allowed: Boolean) { - scope.launch { analyticsDataStore.edit { prefs -> prefs[KEY_ANALYTICS_ALLOWED_PREF] = allowed } } + override fun toggleAnalyticsAllowed() { + scope.launch { + analyticsDataStore.edit { prefs -> + prefs[KEY_ANALYTICS_ALLOWED_PREF] = !(prefs[KEY_ANALYTICS_ALLOWED_PREF] ?: ALLOWED_BY_DEFAULT) + } + } } override val installId: StateFlow = @@ -64,6 +70,8 @@ class AnalyticsPrefsImpl( } companion object { + private const val ALLOWED_BY_DEFAULT = true + private const val ALLOWED_BEFORE_LOAD = false const val KEY_ANALYTICS_ALLOWED = "allowed" const val KEY_INSTALL_ID = "appPrefs_install_id" diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt index bf2e6be0a1..a5cd36101e 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt @@ -30,52 +30,35 @@ import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.prefs.di.AppDataStore import org.meshtastic.core.repository.AppFunctionsPrefs +import org.meshtastic.core.repository.AppFunctionsSetting @Single -@Suppress("TooManyFunctions") class AppFunctionsPrefsImpl(private val dataStore: AppDataStore, dispatchers: CoroutineDispatchers) : AppFunctionsPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) - override val masterEnabled: StateFlow = booleanPref(KEY_MASTER, true) - override val sendMessageEnabled: StateFlow = booleanPref(KEY_SEND_MESSAGE, true) - override val getMeshStatusEnabled: StateFlow = booleanPref(KEY_GET_MESH_STATUS, true) - override val getNodeListEnabled: StateFlow = booleanPref(KEY_GET_NODE_LIST, true) - override val getChannelInfoEnabled: StateFlow = booleanPref(KEY_GET_CHANNEL_INFO, true) - override val getDeviceStatusEnabled: StateFlow = booleanPref(KEY_GET_DEVICE_STATUS, true) - override val getNodeDetailsEnabled: StateFlow = booleanPref(KEY_GET_NODE_DETAILS, true) - override val getMeshMetricsEnabled: StateFlow = booleanPref(KEY_GET_MESH_METRICS, true) - override val getRecentMessagesEnabled: StateFlow = booleanPref(KEY_GET_RECENT_MESSAGES, true) - override val getUnreadSummaryEnabled: StateFlow = booleanPref(KEY_GET_UNREAD_SUMMARY, true) + override val masterEnabled: StateFlow = booleanPref(KEY_MASTER) + override val sendMessageEnabled: StateFlow = booleanPref(KEY_SEND_MESSAGE) + override val getMeshStatusEnabled: StateFlow = booleanPref(KEY_GET_MESH_STATUS) + override val getNodeListEnabled: StateFlow = booleanPref(KEY_GET_NODE_LIST) + override val getChannelInfoEnabled: StateFlow = booleanPref(KEY_GET_CHANNEL_INFO) + override val getDeviceStatusEnabled: StateFlow = booleanPref(KEY_GET_DEVICE_STATUS) + override val getNodeDetailsEnabled: StateFlow = booleanPref(KEY_GET_NODE_DETAILS) + override val getMeshMetricsEnabled: StateFlow = booleanPref(KEY_GET_MESH_METRICS) + override val getRecentMessagesEnabled: StateFlow = booleanPref(KEY_GET_RECENT_MESSAGES) + override val getUnreadSummaryEnabled: StateFlow = booleanPref(KEY_GET_UNREAD_SUMMARY) - override fun setMasterEnabled(enabled: Boolean) = set(KEY_MASTER, enabled) - - override fun setSendMessageEnabled(enabled: Boolean) = set(KEY_SEND_MESSAGE, enabled) - - override fun setGetMeshStatusEnabled(enabled: Boolean) = set(KEY_GET_MESH_STATUS, enabled) - - override fun setGetNodeListEnabled(enabled: Boolean) = set(KEY_GET_NODE_LIST, enabled) - - override fun setGetChannelInfoEnabled(enabled: Boolean) = set(KEY_GET_CHANNEL_INFO, enabled) - - override fun setGetDeviceStatusEnabled(enabled: Boolean) = set(KEY_GET_DEVICE_STATUS, enabled) - - override fun setGetNodeDetailsEnabled(enabled: Boolean) = set(KEY_GET_NODE_DETAILS, enabled) - - override fun setGetMeshMetricsEnabled(enabled: Boolean) = set(KEY_GET_MESH_METRICS, enabled) - - override fun setGetRecentMessagesEnabled(enabled: Boolean) = set(KEY_GET_RECENT_MESSAGES, enabled) - - override fun setGetUnreadSummaryEnabled(enabled: Boolean) = set(KEY_GET_UNREAD_SUMMARY, enabled) - - private fun booleanPref(key: Preferences.Key, default: Boolean): StateFlow = - dataStore.data.map { it[key] ?: default }.stateIn(scope, SharingStarted.Eagerly, default) - - private fun set(key: Preferences.Key, value: Boolean) { - scope.launch { dataStore.edit { prefs -> prefs[key] = value } } + override fun toggle(setting: AppFunctionsSetting) { + val key = setting.key + scope.launch { dataStore.edit { prefs -> prefs[key] = !(prefs[key] ?: ENABLED_BY_DEFAULT) } } } + private fun booleanPref(key: Preferences.Key): StateFlow = + dataStore.data.map { it[key] ?: ENABLED_BY_DEFAULT }.stateIn(scope, SharingStarted.Eagerly, ENABLED_BY_DEFAULT) + companion object { + private const val ENABLED_BY_DEFAULT = true + private val KEY_MASTER = booleanPreferencesKey("appfn_master_enabled") private val KEY_SEND_MESSAGE = booleanPreferencesKey("appfn_send_message") private val KEY_GET_MESH_STATUS = booleanPreferencesKey("appfn_get_mesh_status") @@ -86,5 +69,20 @@ class AppFunctionsPrefsImpl(private val dataStore: AppDataStore, dispatchers: Co private val KEY_GET_MESH_METRICS = booleanPreferencesKey("appfn_get_mesh_metrics") private val KEY_GET_RECENT_MESSAGES = booleanPreferencesKey("appfn_get_recent_messages") private val KEY_GET_UNREAD_SUMMARY = booleanPreferencesKey("appfn_get_unread_summary") + + private val AppFunctionsSetting.key: Preferences.Key + get() = + when (this) { + AppFunctionsSetting.MASTER -> KEY_MASTER + AppFunctionsSetting.SEND_MESSAGE -> KEY_SEND_MESSAGE + AppFunctionsSetting.GET_MESH_STATUS -> KEY_GET_MESH_STATUS + AppFunctionsSetting.GET_NODE_LIST -> KEY_GET_NODE_LIST + AppFunctionsSetting.GET_CHANNEL_INFO -> KEY_GET_CHANNEL_INFO + AppFunctionsSetting.GET_DEVICE_STATUS -> KEY_GET_DEVICE_STATUS + AppFunctionsSetting.GET_NODE_DETAILS -> KEY_GET_NODE_DETAILS + AppFunctionsSetting.GET_MESH_METRICS -> KEY_GET_MESH_METRICS + AppFunctionsSetting.GET_RECENT_MESSAGES -> KEY_GET_RECENT_MESSAGES + AppFunctionsSetting.GET_UNREAD_SUMMARY -> KEY_GET_UNREAD_SUMMARY + } } } diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt index 7322dd9b54..a5ac9a0d54 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.prefs.di.UiDataStore import org.meshtastic.core.repository.MeshBeaconPrefs @@ -46,15 +47,13 @@ class MeshBeaconPrefsImpl(private val dataStore: UiDataStore, dispatchers: Corou .stateIn(scope, SharingStarted.Eagerly, emptyList()) // Single conflated writer: rapid add()/dismiss() calls each publish the full latest list here, and one collector - // serializes the DataStore writes (latest-wins). Avoids launch-per-call races that could persist a stale snapshot. - // runCatching keeps a best-effort write failure (e.g. disk I/O) from escaping as an uncaught coroutine exception — - // the next add/dismiss retries, and on restart we hydrate from the last successful write. + // serializes the DataStore writes (latest-wins). A failed write is dropped; the next add/dismiss retries it. private val pendingWrite = MutableStateFlow?>(null) init { scope.launch { pendingWrite.filterNotNull().collectLatest { records -> - runCatching { dataStore.edit { it[KEY_STORED_BEACONS] = records.joinToString(RECORD_DELIMITER) } } + safeCatching { dataStore.edit { it[KEY_STORED_BEACONS] = records.joinToString(RECORD_DELIMITER) } } } } } diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt index 7e5693ff9e..34b5ba9f9f 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt @@ -36,13 +36,18 @@ class HomoglyphPrefsImpl(private val dataStore: HomoglyphEncodingDataStore, disp private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) override val homoglyphEncodingEnabled: StateFlow = - dataStore.data.map { it[KEY_ENABLED_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + dataStore.data + .map { it[KEY_ENABLED_PREF] ?: ENABLED_BY_DEFAULT } + .stateIn(scope, SharingStarted.Eagerly, ENABLED_BY_DEFAULT) - override fun setHomoglyphEncodingEnabled(enabled: Boolean) { - scope.launch { dataStore.edit { prefs -> prefs[KEY_ENABLED_PREF] = enabled } } + override fun toggleHomoglyphEncodingEnabled() { + scope.launch { + dataStore.edit { prefs -> prefs[KEY_ENABLED_PREF] = !(prefs[KEY_ENABLED_PREF] ?: ENABLED_BY_DEFAULT) } + } } companion object { + private const val ENABLED_BY_DEFAULT = false const val KEY_ENABLED = "enabled" val KEY_ENABLED_PREF = booleanPreferencesKey(KEY_ENABLED) } diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt index 7a6f7da324..4cde0277e4 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt @@ -104,10 +104,16 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp } override val showQuickChat: StateFlow = - dataStore.data.map { it[KEY_SHOW_QUICK_CHAT_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + dataStore.data + .map { it[KEY_SHOW_QUICK_CHAT_PREF] ?: SHOW_QUICK_CHAT_BY_DEFAULT } + .stateIn(scope, SharingStarted.Eagerly, SHOW_QUICK_CHAT_BY_DEFAULT) - override fun setShowQuickChat(show: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_QUICK_CHAT_PREF] = show } } + override fun toggleShowQuickChat() { + scope.launch { + dataStore.edit { + it[KEY_SHOW_QUICK_CHAT_PREF] = !(it[KEY_SHOW_QUICK_CHAT_PREF] ?: SHOW_QUICK_CHAT_BY_DEFAULT) + } + } } override val showFullMessageTimestamps: StateFlow = @@ -332,6 +338,7 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp val KEY_SHOW_NETWORK_TRANSPORT = booleanPreferencesKey("show-network-transport") val KEY_SHOW_USB_TRANSPORT = booleanPreferencesKey("show-usb-transport") private const val MAX_FIRMWARE_UPDATE_NOTIFICATION_KEYS = 100 + private const val SHOW_QUICK_CHAT_BY_DEFAULT = false private fun parseDeviceType(name: String): DeviceType? = DeviceType.entries.firstOrNull { it.name == name } diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/PrefsToggleTest.kt b/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/PrefsToggleTest.kt new file mode 100644 index 0000000000..621d44c8e4 --- /dev/null +++ b/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/PrefsToggleTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import okio.FileSystem +import okio.Path +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.analytics.AnalyticsPrefsImpl +import org.meshtastic.core.prefs.appfunctions.AppFunctionsPrefsImpl +import org.meshtastic.core.prefs.di.asAnalyticsDataStore +import org.meshtastic.core.prefs.di.asAppDataStore +import org.meshtastic.core.prefs.di.asHomoglyphEncodingDataStore +import org.meshtastic.core.prefs.di.asUiDataStore +import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl +import org.meshtastic.core.prefs.ui.UiPrefsImpl +import org.meshtastic.core.repository.AppFunctionsPrefs +import org.meshtastic.core.repository.AppFunctionsSetting +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.uuid.Uuid + +class PrefsToggleTest { + private lateinit var tmpDir: Path + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private lateinit var dispatchers: CoroutineDispatchers + + @BeforeTest + fun setup() { + // Standard, not Unconfined: both toggles must be queued before either write lands. + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "prefsToggleTest-${Uuid.random()}" + FileSystem.SYSTEM.createDirectories(tmpDir) + dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) + } + + @AfterTest + fun tearDown() { + testScope.cancel() + FileSystem.SYSTEM.deleteRecursively(tmpDir) + } + + private fun store(name: String): DataStore = + PreferenceDataStoreFactory.createWithPath(scope = testScope, produceFile = { tmpDir / "$name.preferences_pb" }) + + @Test + fun `quick chat toggle turns the default off state on`() = testScope.runTest { + val dataStore = store("ui") + val prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers) + + prefs.toggleShowQuickChat() + advanceUntilIdle() + + assertEquals(true, dataStore.data.first()[UiPrefsImpl.KEY_SHOW_QUICK_CHAT_PREF]) + } + + @Test + fun `two rapid quick chat toggles both flip and land back on off`() = testScope.runTest { + val dataStore = store("ui") + val prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers) + + prefs.toggleShowQuickChat() + prefs.toggleShowQuickChat() + advanceUntilIdle() + + assertEquals(false, dataStore.data.first()[UiPrefsImpl.KEY_SHOW_QUICK_CHAT_PREF]) + } + + @Test + fun `analytics toggle turns the default allowed state off`() = testScope.runTest { + val dataStore = store("analytics") + val prefs = AnalyticsPrefsImpl(dataStore.asAnalyticsDataStore(), store("app").asAppDataStore(), dispatchers) + + prefs.toggleAnalyticsAllowed() + advanceUntilIdle() + + assertEquals(false, dataStore.data.first()[AnalyticsPrefsImpl.KEY_ANALYTICS_ALLOWED_PREF]) + } + + @Test + fun `two rapid analytics toggles both flip and land back on allowed`() = testScope.runTest { + val dataStore = store("analytics") + val prefs = AnalyticsPrefsImpl(dataStore.asAnalyticsDataStore(), store("app").asAppDataStore(), dispatchers) + + prefs.toggleAnalyticsAllowed() + prefs.toggleAnalyticsAllowed() + advanceUntilIdle() + + assertEquals(true, dataStore.data.first()[AnalyticsPrefsImpl.KEY_ANALYTICS_ALLOWED_PREF]) + } + + @Test + fun `homoglyph toggle turns the default off state on`() = testScope.runTest { + val dataStore = store("homoglyph") + val prefs = HomoglyphPrefsImpl(dataStore.asHomoglyphEncodingDataStore(), dispatchers) + + prefs.toggleHomoglyphEncodingEnabled() + advanceUntilIdle() + + assertEquals(true, dataStore.data.first()[HomoglyphPrefsImpl.KEY_ENABLED_PREF]) + } + + @Test + fun `two rapid homoglyph toggles both flip and land back on off`() = testScope.runTest { + val dataStore = store("homoglyph") + val prefs = HomoglyphPrefsImpl(dataStore.asHomoglyphEncodingDataStore(), dispatchers) + + prefs.toggleHomoglyphEncodingEnabled() + prefs.toggleHomoglyphEncodingEnabled() + advanceUntilIdle() + + assertEquals(false, dataStore.data.first()[HomoglyphPrefsImpl.KEY_ENABLED_PREF]) + } + + @Test + fun `an app functions toggle turns only its own setting off`() = testScope.runTest { + AppFunctionsSetting.entries.forEach { setting -> + val prefs = AppFunctionsPrefsImpl(store("appfn-${setting.name}").asAppDataStore(), dispatchers) + + prefs.toggle(setting) + advanceUntilIdle() + + AppFunctionsSetting.entries.forEach { other -> + assertEquals(other != setting, prefs.enabled(other).value, "toggled $setting then read $other") + } + } + } + + @Test + fun `two rapid app functions toggles both flip and land back on enabled`() = testScope.runTest { + AppFunctionsSetting.entries.forEach { setting -> + val dataStore = store("appfn-${setting.name}") + val prefs = AppFunctionsPrefsImpl(dataStore.asAppDataStore(), dispatchers) + + prefs.toggle(setting) + prefs.toggle(setting) + advanceUntilIdle() + + assertEquals(listOf(true), dataStore.data.first().asMap().values.toList(), setting.name) + } + } + + private fun AppFunctionsPrefs.enabled(setting: AppFunctionsSetting): StateFlow = when (setting) { + AppFunctionsSetting.MASTER -> masterEnabled + AppFunctionsSetting.SEND_MESSAGE -> sendMessageEnabled + AppFunctionsSetting.GET_MESH_STATUS -> getMeshStatusEnabled + AppFunctionsSetting.GET_NODE_LIST -> getNodeListEnabled + AppFunctionsSetting.GET_CHANNEL_INFO -> getChannelInfoEnabled + AppFunctionsSetting.GET_DEVICE_STATUS -> getDeviceStatusEnabled + AppFunctionsSetting.GET_NODE_DETAILS -> getNodeDetailsEnabled + AppFunctionsSetting.GET_MESH_METRICS -> getMeshMetricsEnabled + AppFunctionsSetting.GET_RECENT_MESSAGES -> getRecentMessagesEnabled + AppFunctionsSetting.GET_UNREAD_SUMMARY -> getUnreadSummaryEnabled + } +} diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImplTest.kt b/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImplTest.kt new file mode 100644 index 0000000000..b94a7b2197 --- /dev/null +++ b/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImplTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.analytics + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import okio.FileSystem +import okio.Path +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.asAnalyticsDataStore +import org.meshtastic.core.prefs.di.asAppDataStore +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.uuid.Uuid + +class AnalyticsPrefsImplTest { + private lateinit var tmpDir: Path + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private lateinit var dispatchers: CoroutineDispatchers + + @BeforeTest + fun setup() { + // Standard, not Unconfined: the store must not have loaded when the first read happens. + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + tmpDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "analyticsPrefsTest-${Uuid.random()}" + FileSystem.SYSTEM.createDirectories(tmpDir) + dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) + } + + @AfterTest + fun tearDown() { + testScope.cancel() + FileSystem.SYSTEM.deleteRecursively(tmpDir) + } + + private fun store(name: String): DataStore = + PreferenceDataStoreFactory.createWithPath(scope = testScope, produceFile = { tmpDir / "$name.preferences_pb" }) + + private fun prefs(analytics: DataStore) = + AnalyticsPrefsImpl(analytics.asAnalyticsDataStore(), store("app").asAppDataStore(), dispatchers) + + @Test + fun `a stored opt out is not allowed before or after the store loads`() = testScope.runTest { + val analytics = store("analytics") + analytics.edit { it[AnalyticsPrefsImpl.KEY_ANALYTICS_ALLOWED_PREF] = false } + + val prefs = prefs(analytics) + + assertFalse(prefs.analyticsAllowed.value, "first read, before the store loads") + advanceUntilIdle() + assertFalse(prefs.analyticsAllowed.value, "after the store loads") + } + + @Test + fun `no stored choice is allowed only once the store loads`() = testScope.runTest { + val prefs = prefs(store("analytics")) + + assertFalse(prefs.analyticsAllowed.value, "first read, before the store loads") + advanceUntilIdle() + assertTrue(prefs.analyticsAllowed.value, "after the store loads") + } +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AdminController.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AdminController.kt index e0df46cf93..f94c5b717c 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AdminController.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AdminController.kt @@ -142,7 +142,7 @@ interface AdminController { suspend fun reboot(destNum: Int, packetId: Int) /** Commands a node to reboot into DFU mode. */ - suspend fun rebootToDfu(nodeNum: Int) + suspend fun rebootToDfu(nodeNum: Int, packetId: Int) /** Initiates an OTA reboot request. */ suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt index f97acd2a2b..ed7ce9fa7c 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt @@ -17,13 +17,16 @@ package org.meshtastic.core.repository import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.model.InterfaceId /** Reactive interface for analytics-related preferences. */ interface AnalyticsPrefs { val analyticsAllowed: StateFlow - fun setAnalyticsAllowed(allowed: Boolean) + /** Flips the stored value, not [analyticsAllowed]'s snapshot, which lags a pending write. */ + fun toggleAnalyticsAllowed() val installId: StateFlow } @@ -32,7 +35,8 @@ interface AnalyticsPrefs { interface HomoglyphPrefs { val homoglyphEncodingEnabled: StateFlow - fun setHomoglyphEncodingEnabled(enabled: Boolean) + /** Flips the stored value, not [homoglyphEncodingEnabled]'s snapshot, which lags a pending write. */ + fun toggleHomoglyphEncodingEnabled() } /** Reactive interface for message filtering preferences. */ @@ -122,7 +126,8 @@ interface UiPrefs { val showQuickChat: StateFlow - fun setShowQuickChat(show: Boolean) + /** Flips the stored value, not [showQuickChat]'s snapshot, which lags a pending write. */ + fun toggleShowQuickChat() /** Whether conversation message headers and actions always show both the date and time. */ val showFullMessageTimestamps: StateFlow @@ -342,15 +347,17 @@ interface RadioPrefs { fun setDevName(name: String?) } -fun RadioPrefs.isBle() = devAddr.value?.startsWith("x") == true +/** The saved selection, parsed; `null` when nothing is selected. */ +val RadioPrefs.selectedDevice: DeviceAddress? + get() = DeviceAddress.parse(devAddr.value) -fun RadioPrefs.isSerial() = devAddr.value?.startsWith("s") == true +fun RadioPrefs.isBle() = selectedDevice?.interfaceId == InterfaceId.BLUETOOTH -fun RadioPrefs.isMock() = devAddr.value?.startsWith("m") == true +fun RadioPrefs.isSerial() = selectedDevice?.interfaceId == InterfaceId.SERIAL -fun RadioPrefs.isTcp() = devAddr.value?.startsWith("t") == true +fun RadioPrefs.isMock() = selectedDevice?.interfaceId == InterfaceId.MOCK -fun RadioPrefs.isNoop() = devAddr.value?.startsWith("n") == true +fun RadioPrefs.isTcp() = selectedDevice?.interfaceId == InterfaceId.TCP /** Reactive interface for mesh connection settings. */ interface MeshPrefs { @@ -389,47 +396,35 @@ interface TakPrefs { fun setTakServerChannel(index: Int) } +/** One App Functions switch: the master toggle or a single function's. */ +enum class AppFunctionsSetting { + MASTER, + SEND_MESSAGE, + GET_MESH_STATUS, + GET_NODE_LIST, + GET_CHANNEL_INFO, + GET_DEVICE_STATUS, + GET_NODE_DETAILS, + GET_MESH_METRICS, + GET_RECENT_MESSAGES, + GET_UNREAD_SUMMARY, +} + /** Reactive interface for App Functions (system AI integration) preferences. */ interface AppFunctionsPrefs { val masterEnabled: StateFlow - - fun setMasterEnabled(enabled: Boolean) - val sendMessageEnabled: StateFlow - - fun setSendMessageEnabled(enabled: Boolean) - val getMeshStatusEnabled: StateFlow - - fun setGetMeshStatusEnabled(enabled: Boolean) - val getNodeListEnabled: StateFlow - - fun setGetNodeListEnabled(enabled: Boolean) - val getChannelInfoEnabled: StateFlow - - fun setGetChannelInfoEnabled(enabled: Boolean) - val getDeviceStatusEnabled: StateFlow - - fun setGetDeviceStatusEnabled(enabled: Boolean) - val getNodeDetailsEnabled: StateFlow - - fun setGetNodeDetailsEnabled(enabled: Boolean) - val getMeshMetricsEnabled: StateFlow - - fun setGetMeshMetricsEnabled(enabled: Boolean) - val getRecentMessagesEnabled: StateFlow - - fun setGetRecentMessagesEnabled(enabled: Boolean) - val getUnreadSummaryEnabled: StateFlow - fun setGetUnreadSummaryEnabled(enabled: Boolean) + /** Flips [setting]'s stored value, not its flow's snapshot, which lags a pending write. */ + fun toggle(setting: AppFunctionsSetting) } /** Consolidated interface for all application preferences. */ diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DiscoveryRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DiscoveryRepository.kt new file mode 100644 index 0000000000..4878d31695 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DiscoveryRepository.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.database.entity.DiscoveredNodeEntity +import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity +import org.meshtastic.core.database.entity.DiscoverySessionEntity + +/** Stored Local Mesh Discovery sessions, their per-preset results, and the nodes heard under each preset. */ +interface DiscoveryRepository { + /** Every session, newest first. */ + fun getAllSessions(): Flow> + + fun getSessionFlow(sessionId: Long): Flow + + suspend fun getSession(sessionId: Long): DiscoverySessionEntity? + + fun getPresetResultsFlow(sessionId: Long): Flow> + + suspend fun getPresetResults(sessionId: Long): List + + /** The nodes discovered under each of [presetResultIds], keyed by preset result id in the order given. */ + suspend fun getNodesByPresetResult(presetResultIds: List): Map> + + suspend fun updateSession(session: DiscoverySessionEntity) + + suspend fun updatePresetResult(result: DiscoveryPresetResultEntity) + + /** Deletes the session together with its preset results and their nodes. */ + suspend fun deleteSession(sessionId: Long) + + /** Marks every session still in progress as interrupted, for scans a process death cut short. */ + suspend fun markInterruptedSessions() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareReleaseRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareReleaseRepository.kt index 0ec2089602..0ea4677498 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareReleaseRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareReleaseRepository.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.flow.Flow -import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.FirmwareRelease interface FirmwareReleaseRepository { /** A flow that provides the latest STABLE firmware release. */ diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareUpdateStatusRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareUpdateStatusRepository.kt index 16b3d5c849..ae32cb7144 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareUpdateStatusRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FirmwareUpdateStatusRepository.kt @@ -20,13 +20,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import org.meshtastic.core.resources.UiText data class FirmwareUpdateStatus(val isOtaUpdateActive: Boolean = false, val isAwaitingOtaStatus: Boolean = false) +/** + * What a running firmware update is doing, for surfaces outside the firmware screen. [percent] is null when unknown. + */ +data class FirmwareUpdateProgress(val message: UiText, val percent: Int?) + class FirmwareUpdateStatusRepository { private val _status = MutableStateFlow(FirmwareUpdateStatus()) val status: StateFlow = _status.asStateFlow() + private val _progress = MutableStateFlow(null) + + /** Null whenever no update is transferring, including while the flow waits on the user. */ + val progress: StateFlow = _progress.asStateFlow() + + fun publishProgress(progress: FirmwareUpdateProgress?) { + _progress.value = progress + } + fun beginOtaUpdate() { _status.value = FirmwareUpdateStatus(isOtaUpdateActive = true) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Location.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Location.kt new file mode 100644 index 0000000000..4cd1706f9e --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Location.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +/** + * One location fix from the device. A reading the platform did not report is null, never zero: zero is a real altitude, + * speed and bearing. + */ +data class Location( + val latitude: Double, + val longitude: Double, + /** Height above the WGS84 ellipsoid. */ + val altitudeMeters: Double? = null, + /** Height above mean sea level, where the platform could derive it. */ + val mslAltitudeMeters: Double? = null, + val accuracyMeters: Float? = null, + val speedMetersPerSecond: Float? = null, + val bearingDegrees: Float? = null, + /** UTC epoch milliseconds. */ + val timeMillis: Long, +) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LocationRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LocationRepository.kt index 81e18e79af..05f79bc7bd 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LocationRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LocationRepository.kt @@ -19,9 +19,6 @@ package org.meshtastic.core.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -/** Platform-independent location object for KMP. */ -expect class Location - interface LocationRepository { /** Status of whether the app is actively subscribed to location changes. */ val receivingLocationUpdates: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshBeaconRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshBeaconRepository.kt index ac5bfb1a6a..1aa84b0504 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshBeaconRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshBeaconRepository.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.meshtastic.core.model.MeshBeaconOffer @@ -58,10 +59,13 @@ class MeshBeaconRepository(private val prefs: MeshBeaconPrefs, scope: CoroutineS * rather than on every periodic re-broadcast. */ fun add(offer: MeshBeaconOffer): Boolean { - val isNew = _offers.value.none { it.key == offer.key } - _offers.update { current -> (listOf(offer) + current.filterNot { it.key == offer.key }).take(MAX_OFFERS) } + // Judged against the exact list this update replaced, so two concurrent adds of one key cannot both be new. + val previous = + _offers.getAndUpdate { current -> + (listOf(offer) + current.filterNot { it.key == offer.key }).take(MAX_OFFERS) + } persist() - return isNew + return previous.none { it.key == offer.key } } fun dismiss(key: String) { diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt index fb70805f52..5fb6667549 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt @@ -36,8 +36,11 @@ interface MeshLogRepository { /** Retrieves all [MeshLog]s in the database, up to [maxItem]. */ fun getAllLogs(maxItem: Int = DEFAULT_MAX_LOGS): Flow> - /** Retrieves all [MeshLog]s in the database in the order they were received. */ - fun getAllLogsInReceiveOrder(maxItem: Int = DEFAULT_MAX_LOGS): Flow> + /** + * Emits every [MeshLog] once, oldest first, then completes. The logs are read a page at a time rather than held in + * memory together. + */ + fun readAllLogsInReceiveOrder(): Flow /** Retrieves all [MeshLog]s in the database without any limit. */ fun getAllLogsUnbounded(): Flow> diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshNotificationManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshNotificationManager.kt index 4d72e52ba4..58b30764ce 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshNotificationManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshNotificationManager.kt @@ -17,6 +17,8 @@ package org.meshtastic.core.repository import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.FirmwareUpdateNotice +import org.meshtastic.core.model.MeshBeaconOffer import org.meshtastic.core.model.Node import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry @@ -24,10 +26,9 @@ import org.meshtastic.proto.Telemetry const val SERVICE_NOTIFY_ID = 101 /** - * Mesh-domain notification builder. Provides high-level operations for the message arrival, waypoint, reaction, new - * node, low-battery, and client notification flows specific to this app. Implementations are expected to render the - * platform notification themselves; the generic dispatch primitive is [NotificationManager] (which posts/cancels opaque - * [Notification] records and is *not* domain-aware). + * The one notification API for shared code: every notification the app posts or cancels goes through here, and each + * platform renders it with its own channels, styles and tap targets. [NotificationManager] is the desktop's dispatch + * primitive underneath its implementation, not a second way in. */ @Suppress("TooManyFunctions") interface MeshNotificationManager { @@ -63,13 +64,42 @@ interface MeshNotificationManager { isSilent: Boolean = false, ) - fun showAlertNotification(contactKey: String, name: String, alert: String) + suspend fun showAlertNotification(contactKey: String, name: String, alert: String) - fun showNewNodeSeenNotification(node: Node) + suspend fun showMeshBeaconNotification(offer: MeshBeaconOffer) - fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) + /** [title] arrives resolved: the caller revalidates [node]'s identity right before posting, with no suspension. */ + suspend fun showNewNodeSeenNotification(node: Node, title: String) - fun showClientNotification(clientNotification: ClientNotification) + fun cancelNewNodeNotification(nodeNum: Int) + + /** Posts the low-battery warning for [node], alerting once. */ + suspend fun showLowBatteryNotification(node: Node, isRemote: Boolean) + + /** Refreshes a still-showing low-battery warning with [node]'s current level; never re-posts a dismissed one. */ + suspend fun updateLowBatteryNotification(node: Node, isRemote: Boolean) + + fun cancelLowBatteryNotification(node: Node) + + /** + * [title] and [severity] come from the notification's kind, which shared code classifies once for every platform. + */ + suspend fun showClientNotification( + clientNotification: ClientNotification, + title: String, + severity: Notification.Type, + ) + + fun clearClientNotification(clientNotification: ClientNotification) + + /** True when the platform presents [clientNotification] natively, so the in-app modal must not show it too. */ + fun suppressClientNotificationModal(clientNotification: ClientNotification): Boolean = false + + /** Returns true only when the platform accepted the notification, so the caller can record it as shown. */ + suspend fun showFirmwareUpdateNotification(notice: FirmwareUpdateNotice): Boolean + + /** Returns true only when the platform accepted the notification. [title] and [message] arrive resolved. */ + suspend fun showReconnectBlockedNotification(title: String, message: String): Boolean /** * Suspending because Android rebuilds the group summary here, and the summary's labels come from string resources — @@ -84,8 +114,4 @@ interface MeshNotificationManager { * to dismissing the conversation, which also resolves the spinner. */ suspend fun refreshConversationAfterReply(contactKey: String) = cancelMessageNotification(contactKey) - - fun cancelLowBatteryNotification(node: Node) - - fun clearClientNotification(notification: ClientNotification) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt index 4aa2838e59..11741975bf 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt @@ -183,16 +183,6 @@ interface NodeManager : NodeIdLookup { /** Session-bound counterpart to [updateNode]; deferred persistence is admitted only for [session]. */ fun updateNodeForSession(nodeNum: Int, session: RadioSessionContext, channel: Int = 0, transform: (Node) -> Node) - /** - * Updates a node using a side-effect-free [transform] and awaits any required persistence before returning. The - * transform may be evaluated more than once after compare-and-set contention. - * - * Session-scoped packet processing uses this while holding transport authority so an old session cannot enqueue a - * database write that resumes after a device switch. Non-session UI and controller updates continue to use - * [updateNode]. - */ - suspend fun updateNodeAndPersist(nodeNum: Int, channel: Int = 0, transform: (Node) -> Node) - /** Removes a node from the in-memory database by its number. */ fun removeByNodenum(nodeNum: Int) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Notification.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Notification.kt index 678a78f2a1..6365770057 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Notification.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/Notification.kt @@ -16,21 +16,14 @@ */ package org.meshtastic.core.repository +/** A notification as the desktop dispatch primitive ([NotificationManager]) sees it. */ data class Notification( val title: String, val message: String, val type: Type = Type.Info, val category: Category = Category.Message, - val contactKey: String? = null, val isSilent: Boolean = false, - val group: String? = null, val id: Int? = null, - /** - * Optional deep-link URI invoked when the user taps the notification. Platform implementations are responsible for - * converting this into the appropriate intent / activation action. When null, tapping the notification has no - * effect. - */ - val deepLinkUri: String? = null, ) { enum class Type { None, @@ -46,7 +39,10 @@ data class Notification( Alert, Service, - /** Advisory Mesh Beacon invitations from other meshes — low-importance, its own channel. */ + /** Advisory Mesh Beacon invitations from other meshes. */ MeshBeacon, + + /** Notices from the radio's firmware (ClientNotification). */ + Client, } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NotificationManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NotificationManager.kt index cce1c004c0..f163e24a4e 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NotificationManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NotificationManager.kt @@ -16,14 +16,10 @@ */ package org.meshtastic.core.repository -import org.meshtastic.proto.ClientNotification - /** - * Platform-agnostic notification dispatch primitive. Posts opaque [Notification] records, cancels by id, or wipes all - * active notifications. Intended as the lowest layer of the notification stack. - * - * Domain-specific notification builders (mesh message arrivals, low-battery alerts, etc.) live in - * [MeshNotificationManager], which composes over this dispatcher. + * Dispatch primitive underneath a platform's [MeshNotificationManager]: posts opaque [Notification] records, cancels by + * id, or wipes all active notifications. Shared code never calls it directly; the desktop implementation composes over + * it, while Android renders its notifications natively. */ interface NotificationManager { /** @@ -33,15 +29,6 @@ interface NotificationManager { */ suspend fun dispatch(notification: Notification): Boolean - /** Platform hook for ClientNotifications that should use a native presentation instead of the global modal. */ - fun suppressClientNotificationModal(notification: ClientNotification): Boolean = false - - /** Platform-specific ClientNotification delivery; defaults to the ordinary notification path. */ - suspend fun dispatchClientNotification( - notification: Notification, - clientNotification: ClientNotification, - ): Boolean = dispatch(notification) - fun cancel(id: Int) fun cancelAll() diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 51941a7102..090148a1d9 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -127,14 +127,14 @@ interface PacketRepository { ): PersistedPacketId /** - * Returns a reactive flow of messages for a conversation. + * Returns a reactive flow of messages for a conversation that follows the active database across device switches. * * @param contact The conversation identifier. * @param limit Optional maximum number of messages to return. * @param includeFiltered Whether to include messages that were marked as filtered. * @param getNode Callback to fetch node info for message sender attribution. */ - suspend fun getMessagesFrom( + fun getMessagesFrom( contact: String, limit: Int? = null, includeFiltered: Boolean = true, @@ -206,6 +206,13 @@ interface PacketRepository { /** Deletes messages by their database UUIDs. */ suspend fun deleteMessages(uuidList: List) + /** + * Runs [send], then deletes message [uuid] from the database that was active when [send] started. Nothing is + * deleted if [send] throws or another database has become active meanwhile, since UUIDs are only unique within one + * database. Once [send] returns, cancelling the caller no longer stops the delete. + */ + suspend fun replaceMessage(uuid: Long, send: suspend () -> Unit) + /** Deletes all messages and settings for the given contacts. */ suspend fun deleteContacts(contactList: List) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PlatformAnalytics.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PlatformAnalytics.kt index 733c70c7a5..5cb80cabc1 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PlatformAnalytics.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PlatformAnalytics.kt @@ -71,7 +71,7 @@ interface PlatformAnalytics { * [stopScreenView] using the same [key] when the screen is left. * * @param key A stable identifier that pairs this start with its matching [stopScreenView]. - * @param name The route-derived view name (e.g. `org.meshtastic.core.navigation.NodesRoute.Nodes`). + * @param name The route-derived view name (e.g. `NodesRoute.Nodes`). */ fun startScreenView(key: String, name: String) { // Default no-op for platforms that don't support RUM (fdroid, desktop) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/QuickChatActionRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/QuickChatActionRepository.kt index 94f671fcee..aaabbf402e 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/QuickChatActionRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/QuickChatActionRepository.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.flow.Flow -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction interface QuickChatActionRepository { fun getAllActions(): Flow> diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt index 151a84fe90..010fbb9796 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt @@ -67,9 +67,10 @@ interface RadioSessionAuthority { /** * Runs [block] while holding the same lifecycle lease, without exposing the lease token. Implementations may * serialize this convenience path to preserve handshake ordering; independently deferred work should use - * [runWithSessionLease] so it can acquire its own lease before its parent operation returns. + * [runWithSessionLease] so it can acquire its own lease before its parent operation returns. [label] names the + * operation in diagnostics and must not carry packet contents or identifiers. */ - suspend fun runWhileSessionActive(session: RadioSessionContext, block: suspend () -> Unit): Boolean = + suspend fun runWhileSessionActive(session: RadioSessionContext, label: String, block: suspend () -> Unit): Boolean = runWithSessionLease(session) { block() } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt index f687791ec1..40e2c675a4 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.repository.usecase import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.Capabilities @@ -51,7 +52,16 @@ interface SendMessageUseCase { text: String, contactKey: String = "0${NodeAddress.ID_BROADCAST}", replyId: Int? = null, - ): Int + ): SendMessageOutcome +} + +/** What [SendMessageUseCase] did with a message. */ +sealed interface SendMessageOutcome { + /** Saved to history and queued for delivery under mesh packet id [packetId]. */ + data class Queued(val packetId: Int) : SendMessageOutcome + + /** Nothing was saved or sent: the conversation is retired, so its channel is no longer on the radio. */ + data object Refused : SendMessageOutcome } @Suppress("TooGenericExceptionCaught") @@ -72,13 +82,13 @@ class SendMessageUseCaseImpl( * @param replyId Optional ID of a message being replied to. */ @Suppress("NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod") - override suspend operator fun invoke(text: String, contactKey: String, replyId: Int?): Int { + override suspend operator fun invoke(text: String, contactKey: String, replyId: Int?): SendMessageOutcome { val parsedKey = ContactKey(contactKey) // A retired conversation's channel is no longer on the radio, so there is no slot to send on. Without this // the key's absent channel prefix would read as 0 and the message would go out on the primary channel. if (parsedKey.isRetired) { Logger.w { "Refusing to send to a retired conversation" } - return 0 + return SendMessageOutcome.Refused } val channel = parsedKey.channelOrNull val dest = parsedKey.addressString @@ -145,19 +155,24 @@ class SendMessageUseCaseImpl( "message_send", mapOf("num_bytes" to finalMessageText.length, "is_reply" to (replyId != null)), ) + } catch (e: CancellationException) { + throw e } catch (ex: Exception) { Logger.e(ex) { "Failed to enqueue message packet" } throw ex } - return packetId + return SendMessageOutcome.Queued(packetId) } + // Both side effects are best-effort: the message still goes out if the radio rejects or drops them. private suspend fun favoriteNode(node: Node) { try { radioController.setFavorite(node.num, favorite = true) + } catch (e: CancellationException) { + throw e } catch (ex: Exception) { - Logger.e(ex) { "Favorite node error" } + Logger.w(ex) { "Favorite node error" } } } @@ -167,8 +182,10 @@ class SendMessageUseCaseImpl( if (!accepted) { Logger.w { "Shared contact for node ${node.num} was not acknowledged by the radio" } } + } catch (e: CancellationException) { + throw e } catch (ex: Exception) { - Logger.e(ex) { "Send shared contact error" } + Logger.w(ex) { "Send shared contact error" } } } } diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/AppPreferencesTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/AppPreferencesTest.kt index d1eb7b2e92..a97fb30a99 100644 --- a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/AppPreferencesTest.kt +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/AppPreferencesTest.kt @@ -30,6 +30,20 @@ class AppPreferencesTest { assertTrue(prefs.isBle()) } + @Test + fun `RadioPrefs isBle returns true for the legacy bang prefix`() { + val prefs = FakeRadioPrefs() + prefs.setDevAddr("!12345678") + assertTrue(prefs.isBle()) + } + + @Test + fun `RadioPrefs reports no transport for the none sentinel`() { + val prefs = FakeRadioPrefs() + prefs.setDevAddr("n") + assertFalse(prefs.isBle() || prefs.isSerial() || prefs.isTcp() || prefs.isMock()) + } + @Test fun `RadioPrefs isBle returns false for other prefix`() { val prefs = FakeRadioPrefs() diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessageTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessageTest.kt new file mode 100644 index 0000000000..2dcdc1ecde --- /dev/null +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessageTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +import org.meshtastic.core.model.Position +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.meshtastic.proto.Position as ProtoPosition + +class FixedPositionAdminMessageTest { + + private fun protoPosition(altitude: Int?) = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 525_200_000 + wb.longitude_i = 134_050_000 + wb.altitude = altitude + } + .build() + + @Test + fun `a fixed position without altitude is sent without one`() { + val message = Position(protoPosition(altitude = null)).toFixedPositionAdminMessage() + + assertNull(assertNotNull(message.set_fixed_position).altitude) + } + + @Test + fun `a sea level fixed position is sent as zero`() { + val message = Position(protoPosition(altitude = 0)).toFixedPositionAdminMessage() + + assertEquals(0, assertNotNull(message.set_fixed_position).altitude) + } + + @Test + fun `zero coordinates remove the fixed position whether or not altitude is present`() { + assertEquals(true, Position(0.0, 0.0, null).toFixedPositionAdminMessage().remove_fixed_position) + assertTrue(Position(0.0, 0.0, 0).isFixedPositionRemoval()) + } +} diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/MeshBeaconRepositoryTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/MeshBeaconRepositoryTest.kt new file mode 100644 index 0000000000..a450f553a4 --- /dev/null +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/MeshBeaconRepositoryTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.meshtastic.core.model.MeshBeaconOffer +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.MeshBeacon +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MeshBeaconRepositoryTest { + + private class InMemoryBeaconPrefs : MeshBeaconPrefs { + override val storedBeacons: StateFlow> = MutableStateFlow(emptyList()) + + override fun setStoredBeacons(records: List) = Unit + } + + private fun offer(fromNodeNum: Int) = MeshBeaconOffer( + fromNodeNum = fromNodeNum, + beacon = + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { cs -> cs.name = "Invite" }.build() + } + .build(), + ) + + private fun repository(scope: CoroutineScope) = MeshBeaconRepository(InMemoryBeaconPrefs(), scope) + + @Test + fun `a rebroadcast of a standing invitation is not new`() = runTest { + val repository = repository(backgroundScope) + + assertTrue(repository.add(offer(1))) + assertFalse(repository.add(offer(1))) + assertTrue(repository.add(offer(2))) + } + + @Test + fun `an invitation is new again after it is dismissed`() = runTest { + val repository = repository(backgroundScope) + repository.add(offer(1)) + + repository.dismiss(offer(1).key) + + assertTrue(repository.add(offer(1))) + } + + @Test + fun `concurrent arrivals of one invitation report it new exactly once`() = runTest { + repeat(ROUNDS) { + val repository = repository(backgroundScope) + val results = + withContext(Dispatchers.Default) { List(ARRIVALS) { async { repository.add(offer(7)) } }.awaitAll() } + + assertEquals(1, results.count { it }, "round $it") + } + } + + private companion object { + const val ROUNDS = 200 + const val ARRIVALS = 8 + } +} diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt index 379bc8e9d0..3055e439d4 100644 --- a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt @@ -17,14 +17,20 @@ package org.meshtastic.core.repository.usecase import dev.mokkery.MockMode +import dev.mokkery.answering.calls import dev.mokkery.answering.returns +import dev.mokkery.answering.throws import dev.mokkery.everySuspend import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode import dev.mokkery.verifySuspend import io.kotest.matchers.shouldBe +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.test.runTest +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.repository.MessageQueue @@ -39,6 +45,8 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.User import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertIs class SendMessageUseCaseTest { @@ -78,7 +86,7 @@ class SendMessageUseCaseTest { // Arrange val ourNode = Node(num = 1, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) nodeRepository.setOurNode(ourNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act useCase("Hello broadcast", "0${NodeAddress.ID_BROADCAST}", null) @@ -93,7 +101,7 @@ class SendMessageUseCaseTest { // Arrange val ourNode = Node(num = 1, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) nodeRepository.setOurNode(ourNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act useCase("Hello", "0${NodeAddress.ID_BROADCAST}", null) @@ -112,6 +120,91 @@ class SendMessageUseCaseTest { verifySuspend { messageQueue.enqueue(persistedId) } } + @Test + fun `a queued message reports the packet id it was saved under`() = runTest { + var savedPacket: DataPacket? = null + everySuspend { packetRepository.savePacket(any(), any(), any(), any(), any(), any()) } calls + { call -> + savedPacket = call.arg(2) + PersistedPacketId(myNodeNum = 1, uuid = 10L) + } + + val outcome = useCase("Hello", "0${NodeAddress.ID_BROADCAST}", null) + + val queued = assertIs(outcome) + queued.packetId shouldBe savedPacket?.id + } + + @Test + fun `a retired conversation is refused and nothing is saved or queued`() = runTest { + nodeRepository.setOurNode(Node(num = 1)) + + val outcome = useCase("Hello", ContactKey.retiredBroadcast("token").value, null) + + outcome shouldBe SendMessageOutcome.Refused + verifySuspend(VerifyMode.exactly(0)) { packetRepository.savePacket(any(), any(), any(), any(), any(), any()) } + verifySuspend(VerifyMode.exactly(0)) { messageQueue.enqueue(any()) } + } + + @Test + fun `cancellation while sharing the contact propagates and nothing is queued`() = runTest { + setUpDirectMessage(firmwareVersion = "2.7.12") + radioController.sendSharedContactFailure = CancellationException("scope closed") + + assertFailsWith { useCase("Direct message", "!dest", null) } + + verifySuspend(VerifyMode.exactly(0)) { messageQueue.enqueue(any()) } + } + + @Test + fun `cancellation while favoriting propagates and nothing is queued`() = runTest { + setUpDirectMessage(firmwareVersion = "2.0.0") + radioController.setFavoriteFailure = CancellationException("scope closed") + + assertFailsWith { useCase("Direct message", "!dest", null) } + + verifySuspend(VerifyMode.exactly(0)) { messageQueue.enqueue(any()) } + } + + @Test + fun `a failed contact share still queues the message`() = runTest { + setUpDirectMessage(firmwareVersion = "2.7.12") + radioController.sendSharedContactFailure = RuntimeException("radio down") + + val outcome = useCase("Direct message", "!dest", null) + + assertIs(outcome) + verifySuspend { messageQueue.enqueue(any()) } + } + + @Test + fun `cancellation while saving propagates`() = runTest { + everySuspend { packetRepository.savePacket(any(), any(), any(), any(), any(), any()) } throws + CancellationException("scope closed") + + assertFailsWith { useCase("Hello", "0${NodeAddress.ID_BROADCAST}", null) } + + verifySuspend(VerifyMode.exactly(0)) { messageQueue.enqueue(any()) } + } + + private suspend fun setUpDirectMessage(firmwareVersion: String) { + nodeRepository.setOurNode( + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = firmwareVersion }.build(), + ), + ) + nodeRepository.upsert(Node(num = 12345, user = User.Builder().also { wb -> wb.id = "!dest" }.build())) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false + } + @Test fun `invoke with direct message to older firmware triggers favoriteNode`() = runTest { // Arrange @@ -132,7 +225,7 @@ class SendMessageUseCaseTest { val destNode = Node(num = 12345, user = User.Builder().also { wb -> wb.id = "!dest" }.build()) nodeRepository.upsert(destNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act useCase("Direct message", "!dest", null) @@ -162,7 +255,7 @@ class SendMessageUseCaseTest { val destNode = Node(num = 67890, user = User.Builder().also { wb -> wb.id = "!dest" }.build()) nodeRepository.upsert(destNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act useCase("Direct message", "!dest", null) @@ -177,7 +270,7 @@ class SendMessageUseCaseTest { // Arrange val ourNode = Node(num = 1) nodeRepository.setOurNode(ourNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(true) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = true val originalText = "\u0410pple" // Cyrillic A @@ -208,7 +301,7 @@ class SendMessageUseCaseTest { val destNode = Node(num = 0x70fdde9b.toInt(), user = User.Builder().also { wb -> wb.id = "!70fdde9b" }.build()) nodeRepository.upsert(destNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act — PKI DM: channel 8 + node ID useCase("PKI direct message", "${NodeAddress.PKC_CHANNEL_INDEX}!70fdde9b", null) @@ -239,7 +332,7 @@ class SendMessageUseCaseTest { val destNode = Node(num = 0x12345678, user = User.Builder().also { wb -> wb.id = "!12345678" }.build()) nodeRepository.upsert(destNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act — channel 1 DM (not PKI, not legacy) useCase("Channel DM", "1!12345678", null) @@ -269,7 +362,7 @@ class SendMessageUseCaseTest { val destNode = Node(num = 0xABCDEF01.toInt(), user = User.Builder().also { wb -> wb.id = "!abcdef01" }.build()) nodeRepository.upsert(destNode) - appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) + appPreferences.homoglyph.homoglyphEncodingEnabled.value = false // Act — PKI DM with firmware that doesn't support verified contacts useCase("Old PKI DM", "${NodeAddress.PKC_CHANNEL_INDEX}!abcdef01", null) diff --git a/core/resources/src/commonMain/composeResources/drawable/ic_build.xml b/core/resources/src/commonMain/composeResources/drawable/ic_build.xml new file mode 100644 index 0000000000..bb356d69f3 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/drawable/ic_build.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/resources/src/commonMain/composeResources/values-ar/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ar/schema_strings.xml new file mode 100644 index 0000000000..8b012d2aa4 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ar/schema_strings.xml @@ -0,0 +1,30 @@ + + + + + الحالي + إلغاء + لا يوجد + الاسم + لا يوجد + الجهة + رسالة + لا يوجد + حفظ + استغرق وقت طويل + diff --git a/core/resources/src/commonMain/composeResources/values-ar/strings.xml b/core/resources/src/commonMain/composeResources/values-ar/strings.xml index 1a899307cd..8b54336135 100644 --- a/core/resources/src/commonMain/composeResources/values-ar/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ar/strings.xml @@ -17,6 +17,8 @@ --> + الرطوبة + الحرارة حول قبول @@ -25,7 +27,6 @@ أضف الإدارة - مطلوب تحديث التطبيق تفعيل @@ -34,7 +35,6 @@ هل أنت متيقِّن؟ إعدادات الصوت سيئ - البطارية إعدادات بلوتوث @@ -51,7 +51,6 @@ القناة 6 القناة 7 القناة 8 - عربي اسم القناة القنوات @@ -78,19 +77,17 @@ مباشره الرسائل - إعدادات الشاشة المسافة MQTT + تعديل 8 ساعات - المفتاح العام غير متطابق تشفير المفتاح العام - غلط مناسب @@ -105,10 +102,8 @@ جيد - أنا أعرف ما أفعله. معدل جودة الهواء الداخلي - تجاهل أضف '%1$s' إلى قائمة التجاهل؟ @@ -176,6 +171,7 @@ لا يوجد غير متصل + حسنا @@ -183,21 +179,18 @@ 24 ساعات أسبوع - + - اللغات رمز الاستجابة السريع إعدادات الراديو إعادة التشغيل - - الجهة استبدال @@ -213,7 +206,6 @@ استلم استغرق وقت طويل مؤشر القوة النسبية - حفظ حفظ @@ -221,7 +213,6 @@ إعدادات الحماية ارسل - رقم التسلسلي الإعدادات @@ -251,7 +242,6 @@ اسم المستخدم غير معروف غير المعروفين - اسم المستخدم فقط شبكة MQTT diff --git a/core/resources/src/commonMain/composeResources/values-be/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-be/schema_strings.xml new file mode 100644 index 0000000000..bc6a880231 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-be/schema_strings.xml @@ -0,0 +1,67 @@ + + + + + Сіні + 3 + Зялёны + Чырвоны + Скасаваць + Нічога + Назва + Усе + Усе, і не разбіраць + Толькі асноўныя нумары партоў + Толькі знаёмыя + Толькі мясцовыя + Нічога + CLIENT + CLIENT HIDDEN + CLIENT MUTE + Прылада, якая не перасылае пакеты ад іншых прылад. + LOST AND FOUND + REPEATER + ROUTER + ROUTER CLIENT + ROUTER LATE + SENSOR + TAK + Аптымізавана для сувязі з сістэмай ATAK, змяншае руцінныя трансляцыі. + TAK TRACKER + TRACKER + Транслюе пакеты з GPS-каардынатамі з высокім прыярытэтам. + Тып OLED + Рэгіён + Паведамленне + Пароль + Імя карыстальніка + IP + Нічога + Пароль + SSID + Уключана + + Запісаць + Прыватны ключ + Протобуфы + Скончыўся час чакання + Сервер + Сіні + Зялёны + Чырвоны + diff --git a/core/resources/src/commonMain/composeResources/values-be/strings.xml b/core/resources/src/commonMain/composeResources/values-be/strings.xml index e0cc964e51..a714327a12 100644 --- a/core/resources/src/commonMain/composeResources/values-be/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-be/strings.xml @@ -17,6 +17,8 @@ --> + Вільготнасць + Тэмпература О нас Прыняць @@ -24,7 +26,6 @@ Дадаць Дадаць - Усе Праграма @@ -32,9 +33,7 @@ Прымяніць Аўдыё - Батарэя - Сіні Bluetooth Конфигурация @@ -50,7 +49,6 @@ Канал 6 Канал 7 Канал 8 - Канал Назва канала Каналы @@ -92,7 +90,6 @@ Адлучана Паведамленні - Экран Адлегласць @@ -100,12 +97,11 @@ MQTT Начало работы Сцягнуць + Змяніць 8 Гадзін Уключана - - Асяроддзе Памылка Ethernet IP: @@ -121,14 +117,11 @@ Вольная памяць Пачаць - GPIO - Зялёны Устройства Схаваць пароль Я ведаю, што я раблю. Якасць паветра - Ігнараваць Звесткі @@ -204,16 +197,15 @@ Няма (адключана) Нічога Не злучана + Добра - Тып OLED 24 Гадзін 1 гадзіна 1тыд - Пароль @@ -221,21 +213,17 @@ Месцазнаходжанне тэлефона Граць + Месцазнаходжанне Месцазнаходжанне Пакет месцазнаходжання - Мова - Прыватны ключ QR-код Імгненна даслаць Перазагрузіць - - Чырвоны - Рэгіён Прыбраць @@ -247,30 +235,23 @@ Паспрабаваць яшчэ раз CLIENT - Прылада для паведамленняў, што працуе з прыкладаннем або самастойна. CLIENT HIDDEN CLIENT MUTE - Прылада, якая не перасылае пакеты ад іншых прылад. LOST AND FOUND REPEATER - Інфраструктурны вузел для пашырэння пакрыцця сеткі праз перасылку паведамленняў з мінімальнымі накладнымі выдаткамі. Не бачны ў спісе вузлоў. ROUTER ROUTER CLIENT - Інфраструктурны вузел для пашырэння пакрыцця сеткі праз перасылку паведамленняў. Бачны ў спісе вузлоў. ROUTER LATE SENSOR TAK - Аптымізавана для сувязі з сістэмай ATAK, змяншае руцінныя трансляцыі. TAK TRACKER TRACKER - Транслюе пакеты з GPS-каардынатамі з высокім прыярытэтам. Атрымана адмоўнае пацвярджэнне Няма маршруту Пацверджана Скончыўся час чакання адносная магутнасць - Запісаць Запісаць @@ -279,9 +260,7 @@ Бяспека Вылучыць усе Адправіць - Паслядоўны - Сервер Выберыце ваш рэгіён налады @@ -294,13 +273,9 @@ Прапусціць сігнал-шум Хуткасць - SSID Прыбраць Сервер - Сіні - Зялёны - Чырвоны Тэлеметрыя Тэма Скончыўся час чакання @@ -318,10 +293,8 @@ Нераспазнанае - Карыстальнік - Імя карыстальніка праз MQTT IP-адрас diff --git a/core/resources/src/commonMain/composeResources/values-bg/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-bg/schema_strings.xml new file mode 100644 index 0000000000..eb3a270c03 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-bg/schema_strings.xml @@ -0,0 +1,219 @@ + + + + + Син + Текущ + Зелен + LED за състояние + Червен + По подразбиране + CODEC 2 е активиран + Пин за РТТ + Bluetooth е активиран + Режим на сдвояване + GPIO пин за ротационен енкодер A порт + GPIO пин за ротационен енкодер Б порт + GPIO пин за ротационен енкодер Press порт + Назад + Отказ + Няма + Изберете + Ротационен енкодер #1 е активиран + Приятелско име + Висок + Използване на режим INPUT_PULLUP + GPIO за бутон + GPIO за зумер + Дезактивиране на трикратното щракване + Режим на препредаване + Всички + Само известни + Само локално + Няма + Роля на устройството + Клиент + Устройство за съобщения, свързано с приложение, или самостоятелно. + Скрит клиент + Устройство, което излъчва само при необходимост за скритост или пестене на енергия. + Устройство, което не препредава пакети от други устройства.фигурир + Загубено и намерено + Редовно излъчва местоположението като съобщение до канала по подразбиране, за да подпомогне намирането на устройството. + Ретранслатор + Рутер + Рутер клиент + Сензор + Излъчва приоритетно телеметрични пакети. + TAK + Оптимизирано за комуникация със системата ATAK, намалява рутинните излъчвания. + Тракер + Излъчва приоритетно пакети за GPS позиция + Часова зона + Автоматично превключва към следващата страница на екрана като въртележка, въз основа на зададения интервал. + Компасът на екрана извън кръга винаги ще сочи на север. + Ориентация на компаса + Режим на дисплея + Замяна на оформлението на екрана по подразбиране. + Имперски + Метрични + Обръщане на екрана + Обръщане на екрана вертикално. + Удебелен заглавен шрифт + Удебеляване на текста на заглавието на екрана. + Тип на OLED + Замяна на автоматичното разпознаване на OLED екрана. + Екранът е включен за + Колко дълго екранът остава включен след натискане на потребителския бутон или получаване на съобщения. + Показвани единици + Мерни единици, показвани на екрана на устройството. + Използване на 12ч формат + Когато е активирано, устройството ще показва времето на екрана в 12-часов формат. + Събуждане при докосване или движение + Изисква се да има акселерометър на вашето устройство. + Активно + Външните известия са активирани + Използване на PWM зумер + Широчина на честотната лента + Честотен слот + Брой отскоци + Задава максималния брой отскоци, по подразбиране е 3. Увеличаването на броя отскоци също увеличава претоварването и трябва да се използва внимателно. Съобщенията с 0 отскока няма да получат ACK. + Игнориране на MQTT + Предварително зададени + Регион + Регионът, където ще използвате радиостанциите си. + Бразилия 902 MHz + Китай + Европейски съюз 433MHz + Европейски съюз 868MHz + Индия + Япония + Корея + Малайзия 433 MHz + Малайзия 919 MHz + Непал 865 MHz + Нова Зеландия 865 MHz + Филипини 433 MHz + Филипини 868 MHz + Филипини 915 MHz + Русия + Сингапур 923 MHz + Тайланд + Тайван + Украйна 433 MHz + Украйна 868 MHz + САЩ + Предаването е активирано + Мощност на предаване + Използване на предварително зададени настройки + Медик + Снайперист + Ръководител на екипа + Член на екипа + Съобщение + Адрес + MQTT е активиран + Криптирането е активирано + Парола + Прокси към клиент е активиран + TLS е активиран + Потребителско име + Предаване през LoRa + Интервал на актуализиране + Режим на IPv4 + Ethernet е активиран + Активирането на Ethernet ще деактивира Bluetooth връзката с приложението. TCP връзки с възли не са налични на устройства на Apple. + DNS + Шлюз + IP + Подмрежа + NTP сървър + Няма + rsyslog сървър + Активирането на Wi-Fi ще деактивира Bluetooth връзката с приложението. + Парола + SSID + Праг на BLE RSSI (по подразбиране -80) + Paxcounter е активиран + Интервал на актуализиране + Минимално разстояние + Минимален интервал + Фиксирана позиция + Режим на GPS (физически хардуер) + Интервал на актуализиране + Активиран + Интервал на излъчване + Максималният интервал, който може да изтече, без възела да излъчи позиция. + Интелигентна позиция + Надморска височина + Посока на движение на превозното средство + Брой сателити + Пореден номер + Скорост на превозното средство + Времево клеймо + GPIO за приемане от GPS + GPIO за предаване от GPS + Активиране на енергоспестяващ режим + Изключване при загуба на захранване + Тест на обхвата е активиран + Запазване на .CSV в хранилище (само за ESP32) + Администраторски ключ + Публичният ключ, оторизиран за изпращане на администраторски съобщения до този възел. + Журнали за отстраняване на грешки + Управляем режим + Устройството се управлява от mesh администратор, потребителят няма достъп до никоя от настройките на устройството. + Частен ключ + Използва се за създаване на споделен ключ с отдалечено устройство + Публичен ключ + Серийна конзола + Серийна конзола през Stream API. + Серийна скорост на предаване + Серийна скорост на предаване + Echo е активирано + Серийната връзка е активирана + Сериен режим + RX + По подразбиране + По подразбиране + TX + Сървър + Брой записи + Роля + Син + Кафяв + Циан + Тъмно син + Тъмно зелен + Зелен + Магента + Кестеняв + Оранжев + Лилав + Червен + Тийл + Бял + Жълт + Модулът за показатели за качеството на въздуха е активиран + Интервал на актуализиране на показателите за качеството на въздуха + Изпращане на телеметрия на устройството + Активиране/деактивиране на модула за телеметрия на устройството за изпращане на показатели към мрежата. Това са номинални стойности. Претоварените мрежи автоматично ще се мащабират до по-дълги интервали въз основа на броя на онлайн възлите. + Интервал на актуализиране на показателите на устройството + Показателите на околната среда използват Фаренхайт + Модулът за измерване на околната среда е активиран + Показателите на околната среда на екрана са активирани + Интервал на актуализиране на показателите за средата + diff --git a/core/resources/src/commonMain/composeResources/values-bg/strings.xml b/core/resources/src/commonMain/composeResources/values-bg/strings.xml index 75cb59e30a..a6aec04fe0 100644 --- a/core/resources/src/commonMain/composeResources/values-bg/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-bg/strings.xml @@ -17,15 +17,18 @@ --> + Влажност %1$s: %2$s Съобщение от %1$s: %2$s батерия %1$d% + Канал %1$d любим последно чут %1$s офлайн онлайн роля %1$s сигнал %1$s + Температура Относно Приеми Благодарности @@ -53,7 +56,6 @@ Ръчно добавяне на устройство… Добавяне на мрежов слой Адрес - Администраторски ключ Администраторски ключове Администриране Разширени @@ -61,11 +63,8 @@ Качество на въздуха Икона за качество на въздуха - Модулът за показатели за качеството на въздуха е активиран - Интервал на актуализиране на показателите за качеството на въздуха Процент от ефирното време за предаване, използвано през последния час. Изпол. на ефира - Всички н.в. Надморска височина @@ -91,6 +90,8 @@ Добър Опасно %1$d (%2$s) + Архивиран канал + Този канал вече не е настроен на вашето радио. Съобщенията в него се запазват тук, но не можете да изпращате съобщения или да реагирате. Сигурни ли сте? Сигурни ли сте, че искате да промените канала по подразбиране? Аудио @@ -100,8 +101,6 @@ Запазва публичните и частните ключове в защитено, криптирано хранилище на това устройство. Архивиране & възстановяване Лош - - Широчина на честотната лента По подразбиране (%1$s kHz) %1$s kHz Не се поддържа (%1$s) @@ -110,14 +109,12 @@ Батерия I2C адрес на батерията INA_2XX Bluetooth устройства - Праг на BLE RSSI (по подразбиране -80) - Син + За тази версия на Android сканирането чрез Bluetooth също изисква включени услуги за местоположение. Вашето местоположение не се използва. Bluetooth Налични Bluetooth устройства Конфигуриране на Bluetooth Bluetooth е изключен. Включете го, за да сканирате за устройства наблизо. - Bluetooth е активиран Конфигурация Управлявайте безжично настройките и каналите на вашето устройство. Откриване @@ -130,13 +127,9 @@ Достигнат е лимитът за Bluetooth сканиране. Опитайте отново след %1$d секунда. Достигнат е лимитът за Bluetooth сканиране. Опитайте отново след %1$d секунди. - Удебелен заглавен шрифт Сдвояването не е успешно. Предоставете разрешения на устройството наблизо и опитайте отново. Сдвояването не завърши. Опитайте да сдвоите отново. Настройки - Интервал на излъчване - GPIO за бутон - GPIO за зумер Изчисляване… Опознавателен знак Вашият радиолюбителски опознавателен знак, до 7 знака @@ -157,7 +150,6 @@ Канал 7 Канал 8 URL адресът на този канал е невалиден и не може да се използва - Канал Име на канал Изпол. на канала Канали @@ -184,7 +176,6 @@ Затвори Затваряне на избраните CO₂ - CODEC 2 е активиран Свиване на диаграмата Комуникирайте извън мрежата с вашите приятели и общността без клетъчна услуга. @@ -196,7 +187,6 @@ Необходимо е разрешение за местоположение, за да се покажат разстоянието и пеленгът. Това устройство няма сензор за компас. Посоката не е налична. Север на компаса отгоре - Ориентация на компаса Компас Приблизителна площ: \u00b1%1$s (\u00b1%2$s) Приблизителна площ: неизвестна точност @@ -205,28 +195,12 @@ Изпраща позиция в основния канал, когато потребителският бутон бъде щракнат три пъти. Часова зона за дати на екрана на устройството и в дневника. Използване на часовата зона на телефона - Автоматично превключва към следващата страница на екрана като въртележка, въз основа на зададения интервал. - Компасът на екрана извън кръга винаги ще сочи на север. - Замяна на оформлението на екрана по подразбиране. - Обръщане на екрана вертикално. - Удебеляване на текста на заглавието на екрана. - Замяна на автоматичното разпознаване на OLED екрана. - Колко дълго екранът остава включен след натискане на потребителския бутон или получаване на съобщения. - Мерни единици, показвани на екрана на устройството. - Изисква се да има акселерометър на вашето устройство. - Задава максималния брой отскоци, по подразбиране е 3. Увеличаването на броя отскоци също увеличава претоварването и трябва да се използва внимателно. Съобщенията с 0 отскока няма да получат ACK. + 4/%1$d Налични предварително зададени настройки на модема, по подразбиране е Дълъг Бърз. Регионът, където ще използвате радиостанциите си. - Активирането на Ethernet ще деактивира Bluetooth връзката с приложението. TCP връзки с възли не са налични на устройства на Apple. - Активирането на Wi-Fi ще деактивира Bluetooth връзката с приложението. - Максималният интервал, който може да изтече, без възела да излъчи позиция. - Най-бързо ще бъдат изпратени актуализации на позицията, ако е спазено минималното разстояние. Възелът се рестартира и ще бъде за кратко недостъпен. - Публичният ключ, оторизиран за изпращане на администраторски съобщения до този възел. - Устройството се управлява от mesh администратор, потребителят няма достъп до никоя от настройките на устройството. Използва се за създаване на споделен ключ с отдалечено устройство. Генерира се от вашия частен ключ и се изпраща до други възли в mesh за да им позволи да изчислят споделен секретен ключ. - Серийна конзола през Stream API. Конфигурация Конфигуриране на разрешения за Bluetooth Конфигуриране на критични предупреждения @@ -313,11 +287,8 @@ Метрики на устройството %1$s %1$s: %2$s%% - Интервал на актуализиране на показателите на устройството %1$s: %2$s V Устройството спи - Изпращане на телеметрия на устройството - Активиране/деактивиране на модула за телеметрия на устройството за изпращане на показатели към мрежата. Това са номинални стойности. Претоварените мрежи автоматично ще се мащабират до по-дълги интервали въз основа на броя на онлайн възлите. Тема: %1$s, Език: %2$s Точка на оросяване Директно съобщение @@ -356,24 +327,21 @@ Общо уникални възли Уникални възли Остават %1$s - %1$d уникални възела Вижте картата Свободен диск %1$d - Дисплей - Режим на дисплея - Когато е активирано, устройството ще показва времето на екрана в 12-часов формат. - Показвани единици Разстояние Измервания на разстояния Показване на разстоянието между вашия телефон и други възли на Meshtastic с позиции. - DNS Изчистване на търсенето Зареждане на документацията… Няма налична документация Няма намерени резултати + Отваряне на %1$s + Страницата не е намерена: %1$s + Възможно е тази страница да е преместена или премахната. Търсене в документацията… Ръководство за разработчици Ръководство за потребители @@ -387,6 +355,7 @@ Съобщения & Канали MQTT Възли + Известия Първи стъпки Настройки — Модули & Админ Настройки — Радио & Потребител @@ -400,9 +369,10 @@ Съобщенията от шлюза към публичния интернет се препращат към локалната mesh мрежа. Поради политиката за нулев отскок, трафикът от MQTT сървъра по подразбиране няма да се разпространява по-далеч от това устройство. Изтегляне Открит е дублиран публичен ключ + + Преди %1$s Динамична Настройте лесно частни mesh мрежи за сигурна и надеждна комуникация в отдалечени райони. - Echo е активирано Редактирай 8 часа @@ -414,27 +384,20 @@ Усмивки & емоции Символи Пътуване & места + Неуспешно зареждане на емоджи + Не са открити емоджита Наскоро използвани - Активиране на енергоспестяващ режим Активиран - - Криптирането е активирано Несъответствие на публичния ключ Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие. Криптиране с публичния ключ Показатели на околната среда - Околна среда - Модулът за измерване на околната среда е активиран - Показателите на околната среда на екрана са активирани - Интервал на актуализиране на показателите за средата - Показателите на околната среда използват Фаренхайт Грешка Достигнат лимит на Duty Cycle. Не може да се изпрати съобщение сега, опитайте по-късно. Свързване & администриране Установяване на отдалечена сесия… Опции за Ethernet - Ethernet е активиран Ethernet IP: Размяна на позиция Разгъване на диаграмата @@ -446,7 +409,6 @@ Експортиране на GPX Външно известие Конфигуриране на външни известия - Външните известия са активирани Фабрично нулиране Задоволителен Meshtastic %1$s @@ -456,9 +418,6 @@ - %1$s (%2$d байта) Налични файлове (%1$d): - Дезактивиране на филтрирането - Активиране на филтрирането - Активиране на филтрирането Скриване на %1$d филтрирани Филтър Филтрирани @@ -469,6 +428,11 @@ Версия на фърмуера %1$s приключи. Върнете се към стандартния фърмуер на Meshtastic, за да възстановите нормалните функции. Актуализиране на фърмуера + Последен: %1$s + Инсталиран: %1$s + Последна версия: %1$s. Инсталираната версия се показва, след като устройството се рестартира в режим на актуализация. + Буутлоудърът е актуален + Първо изберете версия на фърмуера, за да може устройството да бъде преинсталирано след изтриване. Това устройство не поддържа изтриване от приложението. Фърмуерът на радиото е твърде стар, за да общува с това приложение. За повече информация относно това вижте нашето ръководство за инсталиране на фърмуер. Възобновяване на актуализацията на фърмуера @@ -496,6 +460,7 @@ Извличане на фърмуера... Неуспешна актуализация Програмиране на устройството, моля изчакайте... + Актуализиране на фърмуера… Дръжте устройството близо до телефона си. Актуализиране до: %1$s Локален файл @@ -507,6 +472,7 @@ Няма свързано устройство Липсва информация за потребителя на възела. Не е намерен фърмуер за %1$s в изданието. + %1$s е инсталиран. Налична е стабилна версия %2$s. Отворете Актуализации на фърмуера, когато сте готови. Отваряне на актуализациите на фърмуера OTA актуализацията не е успешна: %1$s Подготовка на устройството: изтриване на флаша. Това може да отнеме до минута... @@ -525,6 +491,7 @@ Това може да отнеме минута... Цел: %1$s Актуализация на фърмуера + %1$d% Неизвестна грешка Неизвестен модел хардуер: %1$d Качване на фърмуера... @@ -537,9 +504,7 @@ Изчаква се устройството да се рестартира в режим OTA... Чака се устройството да се свърже отново... Версия на фърмуера: %1$s - Фиксиран ПИН Фиксирана позиция - Обръщане на екрана За повече информация вижте нашата политика за поверителност. Удебелен @@ -549,9 +514,6 @@ Свободна памет Налична системна памет в байтове Чест. - Честотен слот - Приятелско име - Шлюз Генериране на QR код Използване на текущия изглед @@ -562,22 +524,14 @@ Започнете GitHub хранилище Добър - GPIO GPIO пин - GPIO пин за ротационен енкодер A порт - GPIO пин за ротационен енкодер Б порт - GPIO пин за ротационен енкодер Press порт - Режим на GPS (физически хардуер) - GPIO за приемане от GPS - GPIO за предаване от GPS - Зелен + По избор. Добавя се към вашия опзнавателен знак, напр. KD2ABC//Attic Heltec Хардуер Модел на хардуера Помощ & Документация Скриване на слоя Скриване на паролата - Брой отскоци Брой отскоци Хост Влаж @@ -586,10 +540,9 @@ Знам какво правя. IAQ (Качество на въздуха в помещенията) относителна скала за IAQ, стойностите са измерени с Bosch BME680. Диапазон на стойностите 0–500. - + IAQ %1$d Игнорирай Добави '%1$s' към списъка с игнорирани? - Игнориране на MQTT Изтрий '%1$s' от списъка с игнорирани? Импортиране на конфигурацията @@ -613,13 +566,13 @@ IP IP адрес: Порт: - Режим на IPv4 %1$s %1$s +%2$d Проверката на ключа е завършена Заявка за проверка на ключ Проверка на ключ + %1$s (%2$s) Филтриране по време на последното чуване: %1$s Последна актуализация на позицията Най-новия алфа @@ -629,8 +582,9 @@ %1$s • %2$s Мрежов слой Научете повече - LED за състояние %1$d библиотеки + Лиценз + Свободен софтуер, разпространяван под лиценза GNU General Public License v3, без гаранция. Можете да го разпространявате повторно при условията на същия лиценз. Лицензиран радиолюбител (Ham) Активирането на тази опция дезактивира криптирането и не е съвместимо с мрежата Meshtastic по подразбиране. @@ -682,18 +636,16 @@ Географска дължина LoRa LoRa + %1$s: %2$s → %3$s Изтощена батерия - Възела %1$s има слаба батерия (%2$d%) + %1$sВъзела %1$s има слаба батерия (%2$d%) Батерията е изтощена: %1$s Открит е слаб ключ за криптиране Управление на слоевете на картата - Управляем режим Изисква се ръчно заявяване на позиция Карта на Mesh - Капацитет: %1$d MB\nИзползвани: %2$d MB Мениджър на кеш - %1$s MB Текущ размер на свалените данни %1$d плочки Изчистване на свалените карти @@ -702,27 +654,42 @@ Сваляне на регион Пълно Изтегляне + На това устройство не е налична карта Филтър на картата\n Всички Дисплей Възли + Показване на игнорираните възли Слоевете на картата поддържат формати .kml, .kmz или GeoJSON. + Непрозрачност: %1$d% %1$s<br>Последно чут: %2$s<br>Последна позиция: %3$s<br>Батерия: %4$s + Изтеглянето е неуспешно – проверете връзката си и опитайте отново + Вече са изтеглени твърде много офлайн региони – първо изтрийте един + Изтеглените региони използват твърде много място за съхранение — първо изтрийте един + Избраната област е твърде голяма — увеличете мащаба или изберете по-малък регион + Изтегляне на терен + Изтегляне… %1$d / %2$d плочки Управление извън линия %1$s, %2$s %3$s Показване на картата Контури + Изтеглянето на терена е завършено + Изтеглянето на терена е неуспешно – проверете връзката си и опитайте отново + Избраната област е твърде голяма за терена — увеличете мащаба или изберете по-малък регион + Изтегляне на терена… %1$d / %2$d плочки + Валежи + Метеорологичен радар Изчистването на SQL кеша е неуспешно, вижте logcat за подробности Свалените SQL данни бяха изчистени успешно за %1$s Съгласие за споделяне на некриптирани данни от възела чрез MQTT С активирането на тази функция, вие потвърждавате и изрично се съгласявате с предаването на географското местоположение на вашето устройство в реално време по протокола MQTT без криптиране. Тези данни за местоположението могат да бъдат използвани за цели като отчитане на карта в реално време, проследяване на устройства и свързани телеметрични функции. - Вашият възел периодично ще изпраща некриптиран пакет с отчет за картата до конфигурирания MQTT сървър, който включва идентификатор, дълго и кратко име, приблизително местоположение, хардуерен модел, роля, версия на фърмуера, LoRa регион, предварително зададена настройка на модема и име на основния канал. Избор на регион за сваляне Започни свалянето Избор на стил на картата посока: %1$d° разстояние: %2$s Прогноза за изтегляне на картинки: %1$s %2$s %3$s + Достигнат е лимитът за плочки (%1$d) Източник на карти Хибриден Нормален @@ -763,6 +730,10 @@ Състояние на доставка на съобщението Това радио се управлява и може да бъде променяно само от отдалечен администратор. Съобщение + Администраторската сесия е изтекла + Администраторският ключ не е оторизиран + Отдалеченият възел не оторизира вашия администраторски ключ. + Невалидна заявка Неуспешно доставяне към mesh Няма радио интерфейс Не можа да се изпрати криптирано съобщение @@ -789,12 +760,11 @@ MQTT Конфигуриране на MQTT - MQTT е активиран Хостът не е намерен Връзката е неуспешна Достъпен. Брокерът е приел идентификационните данни. Достъпен (%1$s) - MQTT прокси на този телефон + Неуспешно TLS ръкостискане: %1$s Свързано Свързване… Прекъсната връзка @@ -825,11 +795,11 @@ Нови възли Напред NFC е деактивиран. Моля, активирайте го в системните настройки. + Азот Уверете се, че сте в обхвата на устройството. Не се виждат Bluetooth устройства Няма избрано устройство - Няма намерени устройства Няма налична статистика Няма заредени слоеве на картата. Няма намерени мрежови устройства @@ -895,6 +865,8 @@ Няма връзка Забележка Бележки + + Mesh Meshtastic използва известия, за да ви държи в течение за нови съобщения и други важни събития. Можете да актуализирате разрешенията си за известия по всяко време от настройките. Известия за канал и директни съобщения. @@ -903,11 +875,8 @@ Известия при получаване на сигнал/позвъняване Известия за получаване на съобщение Сега - NTP сървър - Брой записи Добре - Тип на OLED 24 часа 1 час @@ -923,9 +892,7 @@ Библиотеки с отворен код Отваряне на настройките на Wi-Fi Опции - - Режим на сдвояване Парола PAX @@ -937,7 +904,6 @@ W:%1$d Paxcounter Конфигуриране на Paxcounter - Paxcounter е активиран Периодично излъчване на позиция Meshtastic се нуждае от активирани разрешения за "Устройства наблизо", за да намира и да се свързва с устройства чрез Bluetooth. Можете да ги дезактивирате, когато не се използват. @@ -957,6 +923,7 @@ %1$d секунда %1$d секунди + PM1.0 PM10 PM2.5 @@ -965,7 +932,6 @@ Зададено от текущото местоположение на телефона Позицията е активирана Позиция - Захранване Конфигуриране на захранването Показатели на мощност @@ -982,12 +948,9 @@ Текст Първичен Периодично излъчване на местоположение и телеметрия - Частен ключ Изпращане на местоположение в мрежата Името на доставчика съществува. - Прокси към клиент е активиран PSK - Пин за РТТ Публичен ключ Публичният ключ е променен QR код @@ -1005,19 +968,13 @@ Дъжд (24 ч) Тест на обхвата Конфигуриране на Тест на обхвата - Тест на обхвата е активиран Рестартиране - - Режим на препредаване - Препредава всяко наблюдавано съобщение, ако е било на нашия частен канал или от друга мрежа със същите параметри на lora. Скорошни мрежови устройства Повторно свързване… - Червен Опресняване Обновяване на метаданните Сигурни ли сте, че искате да генерирате отново своя частен ключ?\n\nВъзлите, които може да са обменяли преди това ключове с възела, ще трябва да го премахнат и да обменят отново ключове, за да възобновят защитената комуникация. Регенериране на частния ключ - Регион Отдалечен Отдалечено администриране @@ -1057,26 +1014,14 @@ Роля на устройството Клиент - Третира пакетите от или до предпочитани възли като ROUTER_LATE, а всички останали пакети като CLIENT. - Свързано с приложение или самостоятелно устройство за съобщения. Скрит клиент - Устройство, което излъчва само при необходимост за скритост или пестене на енергия. - Устройство, което не препредава пакети от други устройства.фигурир Загубено и намерено Ретранслатор - Инфраструктурен възел за разширяване на мрежовото покритие чрез препредаване на съобщения с минимални разходи. Не се вижда в списъка с възли. Рутер Рутер клиент - Комбинация от РУТЕР и КЛИЕНТ. Не е за мобилни устройства. - Инфраструктурен възел за разширяване на мрежовото покритие чрез препредаване на съобщения. Вижда се в списъка с възли. - Инфраструктурен възел, който винаги препредава пакети веднъж, но само след всички останали режими, осигурявайки допълнително покритие за локалните клъстери. Вижда се в списъка с възли. Сензор - Излъчва приоритетно телеметрични пакети. TAK - Оптимизирано за комуникация със системата ATAK, намалява рутинните излъчвания. Тракер - Излъчва приоритетно пакети за GPS позиция - Ротационен енкодер #1 е активиран Получено отрицателно потвърждение Неуспешно доставяне към mesh @@ -1089,13 +1034,10 @@ Съобщението е твърде голямо за изпращане RSSI Индикатор за силата на получения сигнал - измерване, използвано за определяне на нивото на получения сигнал, приемано от антената. По-високата стойност на RSSI обикновено показва по-силна и по-стабилна връзка. - rsyslog сървър Сат - Запазване Запазване & рестартиране Запис - Запазване на .CSV в хранилище (само за ESP32) Сканиране Сканиране за Bluetooth устройства @@ -1103,7 +1045,6 @@ Сканиране на NFC Сканиране… Сканиране… - Екранът е включен за Превъртане до края Търсене на емоджи... Вторичен @@ -1134,16 +1075,8 @@ Избрани Избран тип на картата Изпрати - Серийна - Серийна скорост на предаване Конфигуриране на серийната връзка - Серийна конзола - Серийната връзка е активирана - Сериен режим - RX - TX - Сървър Настройване на връзка Задайте вашия регион настройки @@ -1162,7 +1095,6 @@ Показване на пътни точки Изключване Възел: %1$s - Изключване при загуба на захранване ⚠️ Това ще ИЗКЛЮЧИ възела. Ще е необходимо физическо взаимодействие, за да се включи отново. Сигнал Качество на сигнала @@ -1183,15 +1115,12 @@ Използване на текущото местоположение на възела Пропускане Слот - Интелигентна позиция SNR Съотношение сигнал/шум, мярка, използвана в комуникациите за количествено определяне на нивото на желания сигнал спрямо нивото на фоновия шум. В Meshtastic и други безжични системи, по-високото съотношение сигнал/шум показва по-ясен сигнал, който може да подобри надеждността и качеството на предаване на данни. Скорост %1$d Km/h %1$d mph - SSID Останете свързани навсякъде - Подмрежа Успех Продължителност на супер дълбок сън Поддържан @@ -1203,14 +1132,6 @@ TAK (ATAK) Конфигурация на TAK - Роля на члена - Щаб - Медик - Радиотелефонен оператор - Снайперист - Ръководител на екипа - Член на екипа - Неопределена TAK сървър Активиране на локален TAK сървър … @@ -1219,22 +1140,6 @@ Не може да се стартира TAK сървъра. Изключете го и го включете, за да опитате отново. %1$dB ✓ ✗ - Цвят на екипа - Син - Кафяв - Циан - Тъмно син - Тъмно зелен - Зелен - Магента - Кестеняв - Оранжев - Лилав - Червен - Тийл - Неопределен - Бял - Жълт Телеметрия Конфигуриране на телеметрията Темп @@ -1243,9 +1148,7 @@ Светла По подразбиране на системата Време - Часова зона Времево клеймо - TLS е активиран Проследяване на маршрута Трасиране на маршрут @@ -1270,7 +1173,6 @@ Не може да се преведе съобщението Изтеглянето на модела за превод не е успешно - Предаване през LoRa Транспорт API @@ -1283,14 +1185,17 @@ 24Ч 48 часа 2С - Предаването е активирано - Мощност на предаване Тип Въведете съобщение + dBm + kHz + m + Единици По подразбиране на системата Имперски Метрични + Начинът, по който това приложение показва разстояние, надморска височина, скорост и температура. Екранът на самото устройство следва собствените си настройки на дисплея. Неизвестно Неизвестна възраст @@ -1303,7 +1208,6 @@ Включване на звука Неразпознат Не е зададен - 0 - Интервал на актуализиране (секунди) Актуализирано Съобщенията от mesh мрежата ще бъдат изпращани до публичния интернет през конфигурирания шлюз на всеки възел. Време на работа @@ -1312,12 +1216,7 @@ URL не може да бъде празен. Шаблон за URL USB - - Използване на 12ч формат Компактно кодиране за Кирилица - Използване на режим INPUT_PULLUP - Използване на предварително зададени настройки - Използване на PWM зумер Потребител Конфигуриране на потребителя @@ -1325,14 +1224,12 @@ Информация за потребителя Потребителски низ Информация за потребителя - Потребителско име чрез API с MQTT чрез UDP Вижте на картата Преглед на изданието Напрежение - Събуждане при докосване или движение Предупреждение Изтриване на пътна точка? Редактиране на пътна точка diff --git a/core/resources/src/commonMain/composeResources/values-ca/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ca/schema_strings.xml new file mode 100644 index 0000000000..d147dde904 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ca/schema_strings.xml @@ -0,0 +1,36 @@ + + + + + Cancel·lar + Nom + .Mateix comportament que ALL, però sense decodificar paquets, només els reenvia. Només disponible en rol de Repeater. Configurar-ho en qualsevol altre rol resultarà en el comportament ALL. + Ignora els missatges observats de malles externes obertes o que no pot desxifrar. Només reenvia missatges als canals primari/secundari locals del node. + Només permès per als rols SENSOR, TRACKER i TAK_TRACKER; això inhibirà tots els reenviaments, de manera similar al rol CLIENT_MUTE. + Dispositiu que només difon quan cal, per discreció o estalvi d’energia. + Dispositiu sense reenviament de paquets. + Difon regularment la ubicació com a missatge al canal per defecte per ajudar a recuperar el dispositiu. + Difon telemetria com a prioritat + Optimitzat per sistema ATAK, redueix les rutines de difusió. + Activa les difusions automàtiques TAK PLI i redueix les difusions rutinàries. + Difon paquets de posició GPS com a prioritat + Regió + Missatge + Desar + Temps esgotat + diff --git a/core/resources/src/commonMain/composeResources/values-ca/strings.xml b/core/resources/src/commonMain/composeResources/values-ca/strings.xml index 11b41b95a3..d1766d7dc6 100644 --- a/core/resources/src/commonMain/composeResources/values-ca/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ca/strings.xml @@ -25,13 +25,11 @@ Afegir Afegir - Actualització de l'aplicació necessària Aplicar Estàs segur que vols canviar al canal per defecte? - Calculant… Permisos de la càmera @@ -39,7 +37,6 @@ No s'ha pogut canviar el canal perquè la ràdio no està configurada correctament. Si us plau torna-ho a provar. La URL d'aquest canal és invàlida i no es pot fer servir - Canal Nom del canal Escollir tema @@ -70,15 +67,13 @@ Missatge directe Desconnectat - Distància + Editar 8 Hores - - Error Límit del cicle de treball assolit. No es podran enviar més missatges, intenta-ho més tard. @@ -92,8 +87,6 @@ Actualització fallida - - Ignorar Afegir '%1$s' a la llista d'ignorats? Treure '%1$s' de la llista d'ignorats? @@ -112,7 +105,6 @@ Bloquejat - Mida total de la memòria cau: %1$d MB\nMemoria cau feta servir: %2$d MB Director de la memòria cau Mida actual de la memòria cau %1$d tessel·les @@ -173,19 +165,19 @@ Eliminar Cap (desactivat) No connectat + Acceptar 24 Hores - + - Idioma Defecte del sistema @@ -199,14 +191,6 @@ Nova conversa ràpida Configuració de ràdio Reiniciar - - Reenvia qualsevol missatge observat si era al nostre canal privat o d’una altra malla. - .Mateix comportament que ALL, però sense decodificar paquets, només els reenvia. Només disponible en rol de Repeater. Configurar-ho en qualsevol altre rol resultarà en el comportament ALL. - Ignora paquets amb ports no estàndard com: TAK, RangeTest, PaxCounter, etc. Només reenvia paquets amb ports estàndard: NodeInfo, Text, Position, Telemetry i Routing. - Ignora els missatges observats de malles externes com LOCAL ONLY, però va més enllà i també ignora missatges de nodes que no figuren a la llista de nodes coneguts del node. - Ignora els missatges observats de malles externes obertes o que no pot desxifrar. Només reenvia missatges als canals primari/secundari locals del node. - Només permès per als rols SENSOR, TRACKER i TAK_TRACKER; això inhibirà tots els reenviaments, de manera similar al rol CLIENT_MUTE. - Regió Eliminar @@ -217,23 +201,11 @@ Restablir Restablir els defectes - Dispositiu de missatgeria, connectat o autònom. - Dispositiu que només difon quan cal, per discreció o estalvi d’energia. - Dispositiu sense reenviament de paquets. - Node d’infraestructura per ampliar cobertura amb mínima càrrega. Ocult a la llista de nodes. - Combinació ROUTER + CLIENT. No mòbils. - Node d’infraestructura per ampliar cobertura. Apareix a la llista de nodes. - Node d’infraestructura que sempre reenvia els paquets una vegada però només després de tots els altres modes, assegurant cobertura addicional per a clusters locals. Visible a la llista de nodes. - Difon telemetria com a prioritat - Optimitzat per sistema ATAK, redueix les rutines de difusió. - Activa les difusions automàtiques TAK PLI i redueix les difusions rutinàries. - Difon paquets de posició GPS com a prioritat Rebuda confirmació negativa Sense ruta Confirmat Temps esgotat - Desar Desar @@ -241,7 +213,6 @@ Seleccionar tot Enviar - Compartir @@ -268,7 +239,6 @@ Nom d'usuari desconegut No reconeguts - via MQTT Esborrar punt de pas? diff --git a/core/resources/src/commonMain/composeResources/values-cs/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-cs/schema_strings.xml new file mode 100644 index 0000000000..9d3751c4e2 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-cs/schema_strings.xml @@ -0,0 +1,249 @@ + + + + + Modrá + Proud + Zelená + Stav LED + Červená + Výchozí + I2S výstupní data + I2S vstupní data + I2S výběr slov + Bluetooth povoleno + Režim párování + Vytvořit vstupní akci při otáčení proti směru hodinových ručiček + Vytvořit vstupní akci při otáčení ve směru hodinových ručiček + Vytvořit vstupní akci při stisku Press + GPIO pin pro rotační enkodér A port + GPIO pin pro port B rotačního enkodéru + GPIO pin pro port Press rotačního enkodéru + Zpět + Zrušit + Žádný + Vybrat + Rotační enkodér #1 povolen + Odeslat zvonek + Vstup Nahoru/Dolů/Výběr povolen + Typ spouštění detekce + Detekční senzor povolen + GPIO pin ke sledování + Přezdívka + Poslat zvonek s výstražnou zprávou + Vysoká + Použít INPUT_PULLUP režim + Tlačítko GPIO + Bzučák GPIO + Dvojité klepnutí jako stisk tlačítka + Zachází s dvojitým poklepáním na podporovaných akcelerometrech jako se stisknutím uživatelského tlačítka. + Blikání LED při provozu + Interval vysílání Node Info + Režim opětovného vysílání + Vše + Stejné chování jako ALL, ale přeskočí dekódování paketů a jednoduše je znovu vysílá. Dostupné pouze v roli Repeater. Nastavení této možnosti pro jiné role povede k chování jako u ALL. + Ignoruje přijaté zprávy z cizích mesh sítí, které jsou otevřené nebo které nelze dešifrovat. Opakuje pouze zprávy na primárních / sekundárních kanálech místního uzlu. + Žádný + Povoleno pouze pro role SENSOR, TRACKER a TAK_TRACKER. Toto nastavení zabrání všem opakovaným vysíláním, podobně jako role CLIENT_MUTE. + Role zařízení + Zařízení, které vysílá pouze podle potřeby pro utajení nebo úsporu energie. + Zařízení, které nepřeposílá pakety ostatních zařízení. + Pravidelně vysílá polohu jako zprávu do výchozího kanálu a pomáhá tak při hledání ztraceného zařízení. + Senzor + Prioritně vysílá pakety s telemetrií. + TAK + Optimalizované pro systémy komunikace ATAK, snižuje rutinní vysílání. + Povolí automatické vysílání TAK PLI a snižuje běžné vysílání. + Prioritně vysílá pakety s pozicí GPS. + Časové pásmo + Interval přepínání obrazovek + Automaticky přepíná stránky na displeji v daném intervalu. + Vždy ukazovat na sever + Ukazatel kompasu mimo kruh na displeji bude vždy směřovat na sever. + Orientace kompasu + Režim obrazovky + Přepíše výchozí rozložení obrazovky. + Imperiální + Metrický + Překlopit obrazovku + Otočit displej vzhůru nohama. + Tučný nadpis + Zobrazit nadpis na obrazovce tučně. + Typ OLED displeje + Přepsat automatickou detekci OLED displeje. + Obrazovka zapnutá po dobu + Doba, po kterou zůstane displej aktivní po stisku tlačítka nebo po přijetí zprávy. + Zobrazení jednotek + Jednotky, které se zobrazují na displeji zařízení. + Použít 12h formát hodin + Pokud je povoleno, zařízení bude na obrazovce zobrazovat čas ve 12 hodinovém formátu. + Probuzení klepnutím nebo pohybem + Tato funkce vyžaduje, aby vaše zařízení mělo akcelerometr. + Výstupní LED aktivní při HIGH + LED při výstražném zvonku + Bzučák při výstražném zvonku + Vibrace při výstražném zvonku + LED výstražné zprávy + Bzučák výstražné zprávy + Vibrace výstražné zprávy + Externí oznámení povoleno + Výstupní LED (GPIO) + Výstupní pin bzučáku (GPIO) + Výstupní pin vybračního motorku (GPIO) + Použít I2S jako bzučák + Použít PWM bzučák + Šířka pásma + Frekvenční slot + Provozní frekvence vašeho uzlu se vypočítává na základě regionu, předvolby modemu a této hodnoty. Pokud je nastavena na 0, slot se automaticky určí podle názvu primárního kanálu a změní se z výchozího veřejného slotu. Vraťte tuto hodnotu zpět na výchozí veřejný slot, pokud jsou nakonfigurovány soukromé primární a veřejné sekundární kanály. + OK do MQTT + Počet skoků + Nastaví maximální počet skoků, výchozí hodnota je 3. Zvýšení počtu skoků zároveň zvyšuje zahlcení sítě, proto je třeba tuto možnost používat opatrně. Zprávy s 0 skoky (broadcast) neobdrží potvrzení (ACK). + Ignorovat MQTT + Předvolby + Lite - Fast + Lite - Pomalá + Long Range - Fast + Long Range - Moderate + Long Range - Slow + Long Range - Turbo + Medium Range - Fast + Medium Range - Slow + Medium Range - Turbo + Narrow - Fast + Narrow - Slow + Short Range - Fast + Short Range - Slow + Short Range - Turbo + Tiny - Fast + Tiny - Slow + Very Long Range - Slow + Přepsat pracovní cyklus + Ruční nastavení frekvence + Region + Oblast, ve které budete svá rádia používat. + Zvýšené zesílení přijímače (RX) + Vysílání povoleno + Vysílací výkon + Použít předvolbu + Zpráva + Adresa + MQTT povoleno + Šifrování povoleno + Heslo + Proxy na klienta povoleno + Kořenové téma + TLS povoleno + Uživatelské jméno + Informace o sousedech povoleny + Přenos přes LoRa + Umožní odesílat informace o sousedních uzlech (NeighborInfo) nejen do MQTT a PhoneAPI, ale také přes LoRa. Nedostupné na kanálech s výchozím klíčem a názvem. + Interval aktualizace GPS + Režim IPv4 + Povolit vysílání paketů přes UDP v místní síti. + Ethernet povolen + Zapnutí Ethernetu deaktivuje Bluetooth připojení k aplikaci. TCP připojení k uzlu není na zařízeních Apple k dispozici. + DNS + Gateway/Brána + IP adresa + Podsíť + NTP server + Žádný + rsyslog server + Wi-Fi povoleno + Povolením Wi-Fi se deaktivuje připojení aplikace přes Bluetooth. + Heslo + SSID + Práh BLE RSSI (výchozí hodnota -80) + Paxcounter povolen + Interval aktualizace GPS + Chytrá vzdálenost + Minimální změna vzdálenosti v metrech, která se bere v úvahu pro chytré vysílání polohy. + Chytrý Interval + Pevná poloha + Režim GPS (fyzický modul) + Interval aktualizace GPS + Jak často má zařízení zjišťovat polohu pomocí GPS (při intervalu kratším než 10 s zůstává GPS trvale zapnutá). + Povoleno + Interval vysílání + Maximální interval, který může uplynout, aniž by uzel odeslal polohový paket. + Chytrá poloha + Příznaky polohy + Volitelná pole, která se mají zahrnout při sestavování polohových zpráv. Čím více polí je zahrnuto, tím větší bude zpráva – to vede k delší době vysílání a vyššímu riziku ztráty paketů. + Nadm. výška + Časová značka + Vlastní hodnota násobiče pro ADC + Povolit úsporný režim + Uvede zařízení do co nejhlubšího spánku. U rolí tracker a sensor to zahrnuje i vypnutí LoRa rádia. Nepoužívejte toto nastavení, pokud chcete zařízení používat s mobilní aplikací nebo pokud vaše zařízení nemá uživatelské tlačítko. + Vypnutí při ztrátě napájení + Doba čekání na Bluetooth + Test pokrytí povolen + Uložit .CSV do úložiště (pouze ESP32) + Administrátorský klíč + Veřejný klíč oprávněný k odesílání administrátorských zpráv tomuto uzlu. + Ladící protokol API povolen + Živý debug přes sériový port, prohlížení a export logů s anonymizovanou polohou přes Bluetooth. + Řízený režim + Toto zařízení spravuje správce mesh sítě, uživatel nemůže měnit žádná jeho nastavení. + Soukromý klíč + Veřejný klíč + Sériová komunikace + Sériová konzole pomocí Stream API. + Rychlost sériového přenosu + Rychlost sériového přenosu + Povolit echo + Sériová komunikace povolena + Sériový režim + RX + Výchozí + Výchozí + Vypršel čas spojení + TX + Ukládání a předávání povoleno + Pulzující LED + Max. počet záznamů historie + Časové okno historie + Server + Počet záznamů + Role + Modrá + Hnědá + Azurová + Tmavě modrá + Tmavě zelená + Zelená + Purpurová + Vínová + Oranžová + Fialová + Červená + Tyrkysová + Bílá + Žlutá + Modul měření kvality ovzduší povolen + Interval aktualizace měření kvality ovzduší + Odesílat telemetrii zařízení + Povolí/zakáže modul telemetrie zařízení pro odesílání metrik do mesh sítě. Jde o nominální hodnoty. Přetížené mesh sítě se automaticky přizpůsobí na delší intervaly podle počtu online uzlů. + Interval aktualizace metrik zařízení + Měření životního prostředí používá Fahrenheit + Modul měření životního prostředí povolen + Zobrazení měření životního prostředí povoleno + Interval aktualizace měření životního prostředí + Modul měření spotřeby povolen + Měření spotřeby na obrazovce povoleno + Interval aktualizace měření napájení + diff --git a/core/resources/src/commonMain/composeResources/values-cs/strings.xml b/core/resources/src/commonMain/composeResources/values-cs/strings.xml index 344e5b69cd..78fb15e12c 100644 --- a/core/resources/src/commonMain/composeResources/values-cs/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-cs/strings.xml @@ -17,15 +17,18 @@ --> + Vlhkost %1$s: %2$s Zpráva od %1$s: %2$s baterie %1$d% + Kanál: %1$d oblíbené offline online role %1$s signál: %1$s Skok %1$d: %2$d uzlů + Teplota O aplikaci Přijmout Poděkování @@ -43,7 +46,6 @@ Přeložit zprávu Akce Přepsání násobiče ADC - Vlastní hodnota násobiče pro ADC ADC napětí Přidat @@ -55,7 +57,6 @@ Přidat zařízení ručně… Přidat síťovou vrstvu Adresa - Administrátorský klíč Administrátorský klíč Administrace Rozšířené @@ -65,23 +66,13 @@ Kvalita ovzduší Ikona kvality ovzduší Metriky kvality ovzduší - Modul měření kvality ovzduší povolen - Interval aktualizace měření kvality ovzduší Procento vysílacího času použitého během poslední hodiny. AirUtil - - Bzučák při výstražném zvonku - LED při výstražném zvonku Výstražný zvonek! - Vibrace při výstražném zvonku - Bzučák výstražné zprávy - LED výstražné zprávy - Vibrace výstražné zprávy Vše Povolit přiřazení nedefinovaného pinu Výška Nadm. výška - Vždy ukazovat na sever Ambientní osvětlení Nastavení ambientního osvětlení Shromažďujeme analytická data, která nám pomáhají vylepšovat aplikaci pro Android (děkujeme). Získáváme anonymizované informace o chování uživatelů, například hlášení o pádech aplikace, používání jednotlivých obrazovek apod. @@ -114,6 +105,8 @@ AQI Silný %1$d (%2$s) + Archivovaný kanál + Tento kanál už není ve vašem rádiu nastavený. Jeho zprávy zde zůstávají, ale nemůžete odesílat zprávy ani na ně reagovat. Jste si jistý? Opravdu chcete změnit na výchozí kanál? Zvuk @@ -124,8 +117,6 @@ Uloží veřejný a soukromý klíč do zabezpečeného, šifrovaného úložiště tohoto zařízení. Zálohování a obnovení Špatný - - Šířka pásma Výchozí (%1$s kHz) %1$s kHz Nepodporováno (%1$s) @@ -134,15 +125,12 @@ Baterie Adresa INA_2XX I2C baterie Zařízení bluetooth - Práh BLE RSSI (výchozí hodnota -80) Skenování Bluetooth v této verzi Androidu vyžaduje také zapnuté služby určování polohy. Vaše poloha se nepoužívá. - Modrá Bluetooth Dostupná Bluetooth zařízení Nastavení bluetooth Bluetooth je vypnuté. Zapněte jej a vyhledejte blízká zařízení. - Bluetooth povoleno Nastavení Bezdrátová správa nastavení a kanálů zařízení. Objevujte @@ -165,15 +153,11 @@ Byl dosažen limit vyhledávání přes Bluetooth. Zkuste to znovu za %1$d sekund. Byl dosažen limit vyhledávání přes Bluetooth. Zkuste to znovu za %1$d sekund. - Tučný nadpis Párování se nezdařilo. Povolte oprávnění k přístupu k blízkým zařízením a zkuste to znovu. Párování nebylo dokončeno. Zkuste zařízení spárovat znovu. Přístup k zařízením v okolí je vypnutý, takže rádio není dostupné přes Bluetooth. Klepnutím ho znovu povolíte. Meshtastic se nemůže znovu připojit Nastavení - Interval vysílání - Tlačítko GPIO - Bzučák GPIO Pokud tuto hodnotu snížíte, trvale se smaže uložená historie pro %1$d zařízení. Pokud tuto hodnotu snížíte, trvale se smaže uložená historie pro %1$d zařízení. @@ -193,7 +177,6 @@ Přednastavené zprávy povoleny Kanál nelze změnit, protože rádio ještě není připojeno. Zkuste to znovu. Vypnutí není na tomto zařízení podporováno - Interval přepínání obrazovek Využití aktuálního kanálu, včetně dobře vytvořeného TX, RX a poškozeného RX (tzv. šumu). Ch @@ -207,7 +190,6 @@ Kanál 8 Možnosti kanálu Tato adresa URL kanálu je neplatná a nelze ji použít - Kanál Název kanálu URL kanálu ChUtil @@ -260,49 +242,25 @@ Pro zobrazení vzdálenosti a směru je vyžadováno oprávnění k poloze. Toto zařízení nemá kompasový senzor. Směr není k dispozici. Zobrazit sever kompasu nahoře - Orientace kompasu Kompas Odhadovaná oblast: \u00b1%1$s (\u00b1%2$s) Odhadovaná oblast: neznámá přesnost Zjištěny kompromitované klíče, zvolte OK pro obnovení. - Zachází s dvojitým poklepáním na podporovaných akcelerometrech jako se stisknutím uživatelského tlačítka. Nastavuje blikání LED diody na zařízení. U většiny zařízení lze ovládat jednu ze čtyř LED diod, avšak LED diody nabíječky a GPS nelze ovládat. - Umožní odesílat informace o sousedních uzlech (NeighborInfo) nejen do MQTT a PhoneAPI, ale také přes LoRa. Nedostupné na kanálech s výchozím klíčem a názvem. Při trojím stisku tlačítka odeslat polohu na primární kanál. Časové pásmo pro zobrazování dat na displeji a v záznamech (logu). Použít časové pásmo telefonu - Automaticky přepíná stránky na displeji v daném intervalu. - Ukazatel kompasu mimo kruh na displeji bude vždy směřovat na sever. - Přepíše výchozí rozložení obrazovky. - Otočit displej vzhůru nohama. - Zobrazit nadpis na obrazovce tučně. - Přepsat automatickou detekci OLED displeje. - Doba, po kterou zůstane displej aktivní po stisku tlačítka nebo po přijetí zprávy. - Jednotky, které se zobrazují na displeji zařízení. - Tato funkce vyžaduje, aby vaše zařízení mělo akcelerometr. - Provozní frekvence vašeho uzlu se vypočítává na základě regionu, předvolby modemu a této hodnoty. Pokud je nastavena na 0, slot se automaticky určí podle názvu primárního kanálu a změní se z výchozího veřejného slotu. Vraťte tuto hodnotu zpět na výchozí veřejný slot, pokud jsou nakonfigurovány soukromé primární a veřejné sekundární kanály. - Nastaví maximální počet skoků, výchozí hodnota je 3. Zvýšení počtu skoků zároveň zvyšuje zahlcení sítě, proto je třeba tuto možnost používat opatrně. Zprávy s 0 skoky (broadcast) neobdrží potvrzení (ACK). + 4/%1$d + Přidává korekci chyb nad rámec zvoleného profilu. Vyšší kódovací poměr prodlužuje dobu vysílání každého paketu a více zatěžuje povolený vysílací čas a kapacitu kanálu. Předvolby pro tento region jsou určeny pouze licencovaným radioamatérům. Chcete-li je používat, povolte v konfiguraci uživatele režim Licencovaný radioamatér (Ham). Dostupné předvolby modemu, výchozí je Long Fast. Oblast, ve které budete svá rádia používat. - Zapnutí Ethernetu deaktivuje Bluetooth připojení k aplikaci. TCP připojení k uzlu není na zařízeních Apple k dispozici. Povolit vysílání paketů přes UDP v místní síti. - Povolením Wi-Fi se deaktivuje připojení aplikace přes Bluetooth. - Maximální interval, který může uplynout, aniž by uzel odeslal polohový paket. - Minimální změna vzdálenosti v metrech, která se bere v úvahu pro chytré vysílání polohy. - Nejkratší interval, ve kterém budou odesílány aktualizace polohy, pokud byla splněna minimální vzdálenost. - Volitelná pole, která se mají zahrnout při sestavování polohových zpráv. Čím více polí je zahrnuto, tím větší bude zpráva – to vede k delší době vysílání a vyššímu riziku ztráty paketů. - Jak často má zařízení zjišťovat polohu pomocí GPS (při intervalu kratším než 10 s zůstává GPS trvale zapnutá). - Uvede zařízení do co nejhlubšího spánku. U rolí tracker a sensor to zahrnuje i vypnutí LoRa rádia. Nepoužívejte toto nastavení, pokud chcete zařízení používat s mobilní aplikací nebo pokud vaše zařízení nemá uživatelské tlačítko. Uzel se restartuje a bude krátce nedostupný. - Veřejný klíč oprávněný k odesílání administrátorských zpráv tomuto uzlu. - Živý debug přes sériový port, prohlížení a export logů s anonymizovanou polohou přes Bluetooth. - Toto zařízení spravuje správce mesh sítě, uživatel nemůže měnit žádná jeho nastavení. Slouží k vytvoření sdíleného klíče se vzdáleným zařízením. Zařízení nepředává soukromý klíč prostřednictvím vzdálené správy. Nový klíč můžete nastavit, ale nelze jej zpětně načíst. Je generován z vašeho soukromého klíče a odesílán ostatním uzlům v mesh síti, aby mohly vypočítat sdílený tajný klíč. - Sériová konzole pomocí Stream API. Nastavení Nastavení oprávnění Bluetooth Nastavit kritická upozornění @@ -349,7 +307,6 @@ Vymazat všechny filtry Přednastavené filtry Filtry - Ladící protokol API povolen Nejsou k dispozici žádné protokoly aplikace Obnovit Exportovat protokoly @@ -395,8 +352,6 @@ Podrobnosti Detekční senzor Konfigurace detekčního senzoru - Detekční senzor povolen - Typ spouštění detekce Zařízení Nastavení zařízení @@ -411,11 +366,8 @@ Metriky zařízení %1$s %1$s: %2$s%% - Interval aktualizace metrik zařízení %1$s: %2$s V Zařízení spí - Odesílat telemetrii zařízení - Povolí/zakáže modul telemetrie zařízení pro odesílání metrik do mesh sítě. Jde o nominální hodnoty. Přetížené mesh sítě se automaticky přizpůsobí na delší intervaly podle počtu online uzlů. Vzhled: %1$s, Jazyk: %2$s Rosný bod Přímá zpráva @@ -447,6 +399,7 @@ Obnovuji připojení k %1$s Spustit analýzu znovu Relace dokončena + Skenování LoRa předvoleb… Relace nebyla dokončena Průběh vyhledávání Výsledek vyhledávání @@ -486,24 +439,21 @@ Zastavit vyhledávání Analýza pomocí AI není k dispozici %1$s zbývá - %1$d jedinečných uzlů Zobrazit na mapě Volný disk %1$d - Obrazovka Obrazovka zařízení - Režim obrazovky - Pokud je povoleno, zařízení bude na obrazovce zobrazovat čas ve 12 hodinovém formátu. - Zobrazení jednotek Vzdálenost Filtry vzdálenosti Filtrovat seznam uzlů a mesh mapu podle vzdálenosti od vašeho telefonu. Měření vzdálenosti Zobrazí vzdálenost mezi vaším telefonem a ostatními uzly Meshtastic s určenou polohou. - DNS Vymazat hledání + Načítání dokumentace… + Dokumentace není k dispozici + Nenalezen žádný výsledek Hledat v dokumentaci… Příručka vývojáře Uživatelská příručka @@ -532,62 +482,64 @@ Dokumentace Hotovo U tohoto zařízení již nezobrazovat - Dvojité klepnutí jako stisk tlačítka Příjem MQTT dat povolen Zprávy z veřejné internetové brány jsou předávány do místní mesh sítě. Kvůli politice nulového počtu skoků se provoz z výchozího MQTT serveru nebude šířit dál než do tohoto zařízení. Stáhnout + Stáhnout tuto oblast Byl zjištěn duplicitní veřejný klíč + Dynamický Jednoduché nastavení soukromých mesh sítí pro bezpečnou a spolehlivou komunikaci i v odlehlých místech. - Povolit echo Upravit + Upravit zdroj síťových dlaždic 8 hodin + Aktivity + Zvířata a příroda + Jídlo a pití + Objekty + Lidé a tělo + Smajlíci a emoce Symboly Cestování a místa - Povolit úsporný režim + Nelze načíst emoji + Nenalezeny žádné emoji Povoleno - - Šifrování povoleno Neshoda veřejného klíče Veřejný klíč neodpovídá uloženému klíči. Můžete uzel odebrat a nechat jej znovu vyměnit klíče, ale může to znamenat bezpečnostní problém. Kontaktujte uživatele jiným důvěryhodným kanálem a ověřte, zda ke změně klíče došlo v důsledku továrního resetu nebo jiné úmyslné akce. Šifrování veřejného klíče Pro tento uzel je uložen veřejný klíč, takže komunikace s ním používá šifrování veřejným klíčem. Metriky prostředí - Životní prostředí - Modul měření životního prostředí povolen - Zobrazení měření životního prostředí povoleno - Interval aktualizace měření životního prostředí - Měření životního prostředí používá Fahrenheit Chyba Byl dosažen limit pro cyklus. Momentálně nelze odesílat zprávy, zkuste to prosím později. Po opakovaných pokusech se nepodařilo navázat stabilní připojení. Prosím znovu vyberte uzel pro opakování. Vytvoření vzdálené relace… Nastavení Ethernetu - Ethernet povolen Vyžádat pozici Platnost do Exportovat nastavení Exportovat všechny pakety + Exportovat GPX Exportovat databázi uzlů Exportovat datový balíček TAK Externí oznámení Nastavení externího oznámení - Externí oznámení povoleno Obnovení továrního nastavení Slabý Meshtastic %1$s Oblíbené Přidat '%1$s' jako oblíbený uzel? Odstranit '%1$s' z oblíbených uzlů? + - %1$s (%2$d bytes) + Dostupné soubory (%1$d): Přidat slovo nebo regex:vzor - Vypnout filtrování - Zapnout filtrování - Zapnout filtrování - Skrýt zprávy obsahující filtrovaná slova + Zde vypnout filtr + Filtrovat všechny konverzace + Zde zapnout filtr + Skrýt zprávy obsahující filtrovaná slova ve všech kanálech a DM. Pro konkrétní konverzaci lze filtr vypnout v její nabídce. Skrýt %1$d filtrované Filtr Filtrované @@ -601,10 +553,16 @@ Firmware Firmware edice Aktualizovat firmware + Soubor byl zkopírován, ale zařízení nezačalo s mazáním. Zatím se nic nezměnilo. Odpojte zařízení, dvakrát stiskněte jeho resetovací tlačítko a zkuste to znovu. + Soubor se nepodařilo zkopírovat na disk zařízení. Zkontrolujte, zda je disk stále připojený, a zkuste to znovu. + Seznam obrazů pro vymazání není momentálně k dispozici. Zkontrolujte připojení a zkuste to znovu, nebo použijte nástroj pro nahrání firmwaru na flasher.meshtastic.org. Nejprve vyberte verzi firmwaru, aby bylo možné po vymazání zařízení firmware znovu nainstalovat. Vybraný disk není aktualizační disk zařízení. Vyberte disk, který se zobrazí v režimu aktualizace zařízení (obsahuje soubor INFO_UF2.TXT). Vyberte disk zařízení určený pro aktualizaci + Zařízení hlásí jiný zásobní Bluetooth, než se očekává, takže jeho vymazání není bezpečné. Nahlaste prosím tento problém spolu s modelem zařízení. Bootloader tohoto zařízení nelze pomocí této aplikace aktualizovat. + Tato aplikace nemůže ověřit, který Bluetooth zásobník zařízení používá, takže jeho vymazání není bezpečné. Místo toho použijte nástroj pro nahrání firmwaru na flasher.meshtastic.org. + Toto zařízení nepodporuje vymazání z aplikace. Aktualizovat bootloader Do zařízení %1$s se nainstaluje nový bootloader, který výrazně urychlí budoucí aktualizace přes Bluetooth. Poté se znovu nainstaluje firmware.\n\nBěhem procesu zařízení neodpojujte. Aktualizovat bootloader? @@ -706,21 +664,13 @@ Vymazat zařízení během aktualizace Úplně vymaže paměť zařízení a poté od začátku nainstaluje vybraný firmware. Verze firmware: %1$s - Pevný PIN Pevná poloha - Překlopit obrazovku Další informace naleznete v našich zásadách ochrany osobních údajů. Volná paměť Dostupná systémová paměť v bajtech Frekv - Frekvenční slot - Přezdívka Odpor plynu - Gateway/Brána - Vytvořit vstupní akci při otáčení proti směru hodinových ručiček - Vytvořit vstupní akci při otáčení ve směru hodinových ručiček - Vytvořit vstupní akci při stisku Press Vytvořit QR kód Geozóna @@ -745,29 +695,18 @@ Začněte hned GitHub repozitář Silný - GPIO GPIO pin - GPIO pin pro rotační enkodér A port - GPIO pin pro port B rotačního enkodéru - GPIO pin pro port Press rotačního enkodéru - GPIO pin ke sledování - Režim GPS (fyzický modul) Udělit oprávnění - Zelená Volitelné. Přidá se k vašemu volacímu znaku, např. KD2ABC//Attic Heltec Hardware Hardwarový model Směr - Pulzující LED Nápověda a dokumentace Skrýt vrstvu Skrýt heslo - Max. počet záznamů historie - Časové okno historie Žádné uzly v tomto okně Uzly podle počtu skoků - Počet skoků Počet skoků Hostitel Metriky hostitele @@ -775,17 +714,12 @@ Souhlasím. Četl jsem a rozumím výše uvedenému. Dobrovolně souhlasím s nešifrovaným přenosem dat svého uzlu přes MQTT Vím co dělám. - I2S vstupní data - I2S výstupní data - I2S výběr slov IAQ (Vnitřní kvalita ovzduší) relativní hodnota IAQ měřená Bosch BME680. Hodnota rozsahu 0–500. Význam ikon - Ignorovat Přidat '%1$s' do seznamu ignorovaných? Ignorovat příchozí - Ignorovat MQTT Odstranit '%1$s' ze seznamu ignorování? Importovat nastavení @@ -807,7 +741,6 @@ IP adresa IP adresa: Port: - Režim IPv4 JSON výstup povolen %1$s %1$s +%2$d @@ -829,8 +762,6 @@ %1$s • %2$s Odhad pokrytí Zjistit více - Blikání LED při provozu - Stav LED Starý kanál správce %1$d knihoven Licence @@ -927,11 +858,9 @@ Osvětlení Správa vlastních zdrojů dlaždic Správa vrstev mapy - Řízený režim Je vyžadován manuální požadavek na pozici Mapa sítě - Kapacita mezipaměti: %1$d MB\nvyužití mezipaměti: %2$d MB Správce mezipaměti Aktuální velikost mezipaměti %1$d dlaždic @@ -942,6 +871,7 @@ Dokončeno Stahování Mapa není na tomto zařízení k dispozici + Toto sestavení aplikace neobsahuje vykreslování map pro procesor vašeho telefonu. Všechno ostatní funguje normálně. Filtr mapy\n Vše Obrazovka @@ -952,6 +882,7 @@ Mapové vrstvy podporují formáty .kml, .kmz nebo GeoJSON. Průhlednost: %1$d% %1$s<br>Poslední příjem: %2$s<br>Poslední pozice: %3$s<br>Baterie: %4$s + Offline – zobrazení dat mapy v mezipaměti Stahování se nezdařilo — zkontrolujte připojení a zkuste to znovu Stáhnout terén Stahování… %1$d / %2$d dlaždic @@ -965,11 +896,8 @@ Meteorologický radar Vyčištění mezipaměti SQL selhalo, podrobnosti naleznete v logcat Mezipaměť SQL vyčištěna pro %1$s - Hlášení mapy Souhlas se sdílením nešifrovaných dat uzlu prostřednictvím MQTT Povolením této funkce potvrzujete a výslovně souhlasíte s přenosem zeměpisné polohy vašeho zařízení v reálném čase přes MQTT protokol bez šifrování. Tato lokalizační data mohou být použita pro účely, jako je hlášení živých map, sledování zařízení a související telemetrické funkce. - Interval hlášení mapy - Váš uzel bude pravidelně odesílat nešifrovaný mapový paket na konfigurovaný MQTT server, který zahrnuje id, dlouhé a krátké jméno. přibližné umístění, hardwarový model, role, verze firmwaru, region LoRa, předvolba modemu a název primárního kanálu. Vyberte oblast stahování Zahájit stahování Výběr stylu mapy @@ -992,12 +920,13 @@ Pozvánky do mesh sítí Naslouchat majákům Zachytávat pozvánky vysílané okolními mesh sítěmi - Majáková zpráva + Žádné dostupné kanály Blízká mesh síť vás pozvala k připojení Pozvánka do mesh sítě Přidat kanál Kanál: %1$s Zavřít + Od neznámého uzlu Předvolba: %1$s Region: %1$s Signál: %1$s / %2$s @@ -1069,9 +998,6 @@ %1$s %2$d μg/m3 Min - Minimální vysílání (sekundy) - Chytrá vzdálenost - Chytrý Interval Minimální doba probuzení Předvolby Nastavení modulů @@ -1080,7 +1006,6 @@ MQTT Nastavení MQTT - MQTT povoleno MQTT: připojení bylo ztraceno MQTT: připojení odmítnuto (ověřte přihlašovací údaje) Proxy MQTT selhala: %1$s @@ -1088,8 +1013,6 @@ Hostitel nebyl nalezen Připojení se nezdařilo Dosažitelný. Broker přijal přihlašovací údaje. - MQTT proxy na tomto telefonu - Tento telefon přeposílá MQTT provoz pro připojené zařízení. Vypnutím okamžitě zastavíte přenos (relay), aniž byste museli měnit nastavení MQTT v zařízení — užitečné v případě, že MQTT provoz zahlcuje připojení. Opětovným zapnutím obnovíte přeposílání. Připojeno Odpojeno Neaktivní @@ -1108,7 +1031,6 @@ Ztlumeno na %1$d dní, %2$s hodiny Ztlumeno na %1$s hodin Neztlumeno - Interval opakovaného zvonění Jméno Název nesmí být prázdný. Přejít zpět @@ -1117,7 +1039,6 @@ Pro používání Meshtastic potřebujete kompatibilní zařízení. Naši podporovatelé a partneři nabízejí zařízení připravená k použití. Zde jsou některé z nejoblíbenějších možností. Informace o sousedech Nastavení informace o sousedech - Informace o sousedech povoleny Síť Nová URL kanálu přijata Nové zprávy @@ -1129,7 +1050,6 @@ Ujistěte se, že jste v dosahu zařízení. Nebyla nalezena žádná zařízení Bluetooth Není vybráno žádné zařízení - Zařízení nenalezena Žádné statistiky k dispozici Žádné vlastní vrstvy nenačteny. Nenalezena žádná síťová zařízení @@ -1201,7 +1121,6 @@ Oblíbené přes MQTT Reset NodeDB - Interval vysílání Node Info Uzly Uzly na tomto místě @@ -1223,6 +1142,8 @@ Teď ne Poznámka Poznámka + + Mesh Oznámení jsou vypnutá a Android se už nezeptá. Zapněte je v nastavení aplikace, abyste dostávali upozornění na nové zprávy a další události. Bez oznámení vás Meshtastic nemůže upozornit na nové zprávy, nové uzly ani nízký stav baterie, když aplikace běží na pozadí. Oznámení vás upozorní, když aplikace Meshtastic není otevřená: na nové zprávy, nově nalezené uzly a nízký stav baterie rádia. Pokud je zamítnete, nic dalšího se nezmění. @@ -1234,8 +1155,6 @@ Oznámení při příjmu výstrahy/zvonku Oznámení při příjmu zprávy Nyní - NTP server - Počet záznamů Offline mapy Zatím nebyly staženy žádné oblasti @@ -1243,9 +1162,7 @@ Zatím nebyl stažen žádný terén Offline terén Obsahuje podrobný terén dané oblasti - OK do MQTT OK - Typ OLED displeje 24 hodin 1 hodina @@ -1262,21 +1179,14 @@ Open source knihovny Otevřít nastavení Wi-Fi Nastavení - - Výstupní pin bzučáku (GPIO) - Doba trvání výstupu (v milisekundách) - Výstupní LED aktivní při HIGH - Výstupní LED (GPIO) - Výstupní pin vybračního motorku (GPIO) + ORP Přepsat sériový port komunikace - Přepsat pracovní cyklus - Ruční nastavení frekvence Pravost paketu Vyvážená — upřednostňovat ověřené Doporučeno. Odmítat pokusy o přechod na nepodepsaný provoz u uzlů, o kterých je známo, že používají podpisy. Kompatibilní — přijímat nepodepsané - Pokud je to možné, ověřovat autenticitu paketů, ale pro maximální kompatibilitu přijímat i nepodepsaný provoz. + Přijímat nepodepsaný provoz pro maximální kompatibilitu. Pokud lze podpis ověřit a je neplatný, paket se přesto zahodí. Úroveň ochrany Přísná — vyžadovat ověření Povolit přísný režim @@ -1284,7 +1194,6 @@ Zobrazovat a zpracovávat pouze kryptograficky ověřené pakety mesh sítě. Starší uzly a příliš velké pakety nemusí být dostupné. Povolit přísné ověřování? Toto připojené zařízení nepodporuje ověřování kryptografických podpisů paketů. - Režim párování Heslo PAX @@ -1295,7 +1204,6 @@ Wi-Fi: %1$s Paxcounter Nastavení Paxcounteru - Paxcounter povolen Pravidelné vysílání polohy Skenovat QR kódy kanálů a kontaktů @@ -1342,6 +1250,12 @@ %1$d sekund %1$d sekund + + Stav senzoru + Chyba CO₂ + Chyba ventilátoru + Chyba senzoru plynu + Chyba PM PM1.0 PM10 PM2.5 @@ -1349,17 +1263,13 @@ Pozice Použít aktuální polohu telefonu Pozice povolena - Příznaky polohy Pozice Polohový paket - Napájení Nastavení napájení Metriky napájení - Modul měření spotřeby povolen - Měření spotřeby na obrazovce povoleno - Interval aktualizace měření napájení Napájeno + ppm Přesná poloha Jazyk Podle systému @@ -1369,10 +1279,8 @@ Primární Pravidelné vysílání pozice a telemetrie - Soukromý klíč Informace o projektu Poskytnout polohu síti - Proxy na klienta povoleno PSK Veřejný klíč Veřejný klíč změněn @@ -1391,24 +1299,13 @@ Déšť (24h) Zkouška dosahu Nastavení testu pokrytí - Test pokrytí povolen Odpovědět Restartovat - - Režim opětovného vysílání - Znovu odeslat jakoukoli pozorovanou zprávu, pokud byla na našem soukromém kanálu nebo z jiné sítě se stejnými parametry lory. - Stejné chování jako ALL, ale přeskočí dekódování paketů a jednoduše je znovu vysílá. Dostupné pouze v roli Repeater. Nastavení této možnosti pro jiné role povede k chování jako u ALL. - Ignoruje pakety z nestandardních portů, jako jsou: TAK, RangeTest, PaxCounter atd. Opakuje pouze pakety se standardními porty: NodeInfo, Text, Position, Telemetry a Routing. - Ignoruje přijaté zprávy z cizích mesh sítí, jako je LOCAL ONLY, ale jde ještě o krok dál tím, že také ignoruje zprávy od uzlů, které již nejsou v seznamu známých uzlů daného uzlu. - Ignoruje přijaté zprávy z cizích mesh sítí, které jsou otevřené nebo které nelze dešifrovat. Opakuje pouze zprávy na primárních / sekundárních kanálech místního uzlu. - Povoleno pouze pro role SENSOR, TRACKER a TAK_TRACKER. Toto nastavení zabrání všem opakovaným vysíláním, podobně jako role CLIENT_MUTE. Nedávná síťová zařízení - Červená Obnovit Obnovit metadata Jste si jisti, že chcete obnovit svůj soukromý klíč?\n\nUzly, které si již vyměnily klíče s tímto uzlem, budou muset odebrat tento uzel a vyměnit klíče pro obnovení bezpečné komunikace. Obnovit soukromý klíč - Region Zachyceno %1$d přeposlání Zachyceno %1$d přeposlání @@ -1459,23 +1356,8 @@ Importované vyzvánění Role zařízení - Pakety od oblíbených uzlů nebo směrované k nim jsou označeny jako ROUTER_LATE, ostatní pakety jako CLIENT. - Připojená aplikace nebo nezávislé zařízení. - Zařízení, které vysílá pouze podle potřeby pro utajení nebo úsporu energie. - Zařízení, které nepřeposílá pakety ostatních zařízení. - Pravidelně vysílá polohu jako zprávu na výchozí kanál, aby usnadnila nalezení zařízení. - Uzel infrastruktury pro rozšíření pokrytí sítě přenosem zpráv s minimální režií. Není viditelné v seznamu uzlů. - Kombinace ROUTER a CLIENT. Ne u mobilních zařízení. - Uzel infrastruktury pro rozšíření pokrytí sítě přeposíláním zpráv. Viditelné v seznamu uzlů. - Uzel infrastruktury, který vždy jednou zopakuje pakety, ale až po všech ostatních režimech, čímž zajišťuje lepší pokrytí místních clusterů. Je viditelný v seznamu uzlů. Senzor - Prioritně vysílá pakety s telemetrií. TAK - Optimalizované pro systémy komunikace ATAK, snižuje rutinní vysílání. - Povolí automatické vysílání TAK PLI a snižuje běžné vysílání. - Prioritně vysílá pakety s pozicí GPS. - Kořenové téma - Rotační enkodér #1 povolen Seznámil jsem se s <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">dokumentací k rolím zařízení</a> a příspěvkem na blogu o <a href="http://meshtastic.org/blog/choosing-the-right-device-role">výběru správné role zařízení</a>. Klíč správce není autorizován @@ -1492,13 +1374,10 @@ Zpráva je příliš dlouhá pro odeslání RSSI Indikátor síly přijímaného signálu, měření, které se používá k určení hladiny výkonu přijímané anténou. Vyšší hodnota RSSI obvykle znamená silnější a stabilnější spojení. - rsyslog server Satelitů - Uložit Uložit a restartovat Uložit - Uložit .CSV do úložiště (pouze ESP32) Exportovat testovací pakety dosahu Skenovat @@ -1512,7 +1391,6 @@ Naskenovat QR kód sdíleného kontaktu Vyhledávání… Vyhledávání… - Obrazovka zapnutá po dobu Hledat emoji... Hledat zprávy… Sekundární @@ -1548,24 +1426,14 @@ Vybrat vše Vybrané Odeslat - Odeslat zvonek - Poslat zvonek s výstražnou zprávou - Interval odesílání zpráv - Sériový - Rychlost sériového přenosu Konfigurace sériové komunikace - Sériová komunikace - Sériová komunikace povolena - Sériový režim - RX - TX - Server Relace aktivní Nastavení času Nastavit připojení Nastavte svůj region nastavení + Vyhledávání nastavení Sdílet Sdílet odkaz @@ -1594,7 +1462,6 @@ Zobrazit trasové body Vypnout Uzel: %1$s - Vypnutí při ztrátě napájení ⚠️ Tímto dojde k VYPNUTÍ uzlu. K jeho opětovnému zapnutí bude nutný fyzický zásah. Signál Kvalita signálu @@ -1619,8 +1486,8 @@ Použít aktuální polohu Použít střed mapy Použít aktuální polohu uzlu + Systémový WebView se aktualizuje. Zkuste to za chvíli znovu. Přeskočit - Chytrá poloha SNR Poměr signálu k šumu (SNR) je veličina používaná k vyjádření poměru mezi úrovní požadovaného signálu a úrovní šumu na pozadí. V Meshtastic a dalších bezdrátových systémech vyšší hodnota SNR značí čistší signál, což může zvýšit spolehlivost a kvalitu přenosu dat. Vlhkost půdy @@ -1628,29 +1495,23 @@ Rychlost %1$d Km/h %1$d mph - SSID - Vysílání stavu (v sekundách) Stavová zpráva Veřejná stavová zpráva vysílaná do mesh sítě při změně a každých 12 hodin. Zůstaňte připojeni kdekoliv Zastavit připojování Ukládání a předávání Nastavení ukládání a předávání - Ukládání a předávání povoleno - Podsíť Doba super hlubokého spánku Podporované Podporováno komunitou Meshtastic + Hardware nezávislého výrobce Smazat Ztlumit Zrušit ztlumení - Zvýšené zesílení přijímače (RX) Nastavení systému TAK (ATAK) Nastavení TAK - Role člena - Nespecifikováno TAK server TAK mesh kanál Kanál Meshtastic používaný pro odchozí provoz TAK @@ -1677,22 +1538,6 @@ Není v schématu v1 (očekáváno) ✗ Firmware připojeného uzlu nepodporuje plnou integraci TAK – do ATAK se budou přenášet pouze polohy a chatové zprávy. Značky a další typy událostí vyžadují firmware verze 2.8.0 nebo novější. - Barva týmu - Modrá - Hnědá - Azurová - Tmavě modrá - Tmavě zelená - Zelená - Purpurová - Vínová - Oranžová - Fialová - Červená - Tyrkysová - Nespecifikováno - Bílá - Žlutá Telemetrie Nastavení telemetrie Teplota @@ -1701,10 +1546,8 @@ Světlý Podle systému Čas - Časové pásmo Vypršel čas spojení Časová značka - TLS povoleno Zapnout/vypnout pozici Traceroute @@ -1740,11 +1583,11 @@ Překlad zpráv vyžaduje stažení jednorázového modelu (o %1$d MB). Stáhnout překladový model? Stahování překladového modelu… + Zprávu nelze přeložit Stažení překladového modelu se nezdařilo Zpráva je již ve vašem jazyce Vysílání je vypnuté Toto zařízení může přijímat, ale přes LoRa nebude nic vysílat. - Přenos přes LoRa Přenos API @@ -1758,8 +1601,6 @@ 24H 48 hodin 2T - Vysílání povoleno - Vysílací výkon Typ Napište zprávu UDP vysílání @@ -1781,9 +1622,6 @@ Zrušit ztlumení Neznámý Nenastaveno – 0 - Vstup Nahoru/Dolů/Výběr povolen - Interval aktualizace GPS - Interval aktualizace (v sekundách) Aktualizovat stav Aktualizováno Odesílání MQTT dat povoleno @@ -1797,13 +1635,7 @@ URL šablona USB Přístup k USB byl zamítnut. Odpojte a znovu připojte zařízení a zkuste to znovu. - - Použít 12h formát hodin Úsporné kódování pro cyriliku - Použít I2S jako bzučák - Použít INPUT_PULLUP režim - Použít předvolbu - Použít PWM bzučák Uživatel Nastavení uživatele @@ -1811,7 +1643,6 @@ Informace o uživateli Uživatelský řetězec Informace o uživateli - Uživatelské jméno UV lux přes API přes MQTT @@ -1819,8 +1650,6 @@ Zobrazit na mapě Zobrazit vydání Napětí - Doba čekání na Bluetooth - Probuzení klepnutím nebo pohybem Varování Smazat waypoint? Upravit waypoint @@ -1833,20 +1662,22 @@ Možnosti Wi-Fi Nastavení Wi-Fi pro mPWRD-OS - Wi-Fi povoleno Dostupné sítě Nelze se připojit: %1$s Nastavit přihlašovací údaje k Wi-Fi v zařízení mPWRD-OS přes Bluetooth. Zařízení nalezeno + Připraveno prohledávat Wi-Fi sítě. Skrytá síť Více informací o projektu mPWRD-OS\nhttps://github.com/mPWRD-OS Žádná síť nebyla nalezena + Nepodařilo se prohledat Wi-Fi sítě: %1$s Vyhledat sítě Vyhledávání zařízení… Vyhledávání… %1$d% Název sítě (SSID) Zadejte nebo vyberte síť + Nepodařilo se použít nastavení Wi-Fi Vaše zařízení mPWRD-OS se připojilo k Wi-Fi síti. Zařízení připojeno Hotovo diff --git a/core/resources/src/commonMain/composeResources/values-de/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-de/schema_strings.xml new file mode 100644 index 0000000000..047d3ace0b --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-de/schema_strings.xml @@ -0,0 +1,329 @@ + + + + + Blau + Stromstärke + Grün + LED Zustand + Rot + Standardwert + CODEC2 Abtastrate + CODEC 2 aktiviert + I2S Daten Ausgang + I2S Takt + I2S Daten Eingang + I2S Wortauswahl + GPIO Pin PTT + Bluetooth aktiviert + Feste PIN + Kopplungsmodus + Feste PIN + Keine PIN (geht einfach) + Eingabeereignis bei Gegenuhrzeigersinn generieren + Eingabeereignis bei Uhrzeigersinn generieren + Eingabeereignis beim Drücken generieren + GPIO-Pin für Drehencoder A Port + GPIO-Pin für Drehencoder B Port + GPIO-Pin für Drehencoder Knopf Port + Zurück + Abbrechen + Runter + Links + Keins + Rechts + Auswählen + Hoch + Drehencoder #1 aktiviert + Glocke senden + Up/Down/Select Eingang aktiviert + Typ der Erkennungsauslösung + Erkennungssensor aktiviert + Zu überwachender GPIO-Pin + Anzeigename + Glocke mit Warnmeldung senden + Übertragungsintervall + Hoch + Eingang PULLUP Einstellung + GPIO Taste + GPIO Summer + Dreifachklick deaktivieren + Doppelklick als Taste + Behandle doppeltes Antippen mit unterstützten Beschleunigungssensoren wie einen Benutzer-Tastendruck. + Puls LED + Knoteninfo Übertragungsintervall + Weiterleitungsmodus + Alle + Alle, überspringe Dekodierung + Das gleiche Verhalten wie ALLE aber überspringt die Paketdekodierung und sendet sie einfach erneut. Nur in Repeater Rolle verfügbar. Wenn Sie diese auf jede andere Rolle setzen, wird ALLE Verhaltensweisen folgen. + Nur Kernanschlussnummern + Nur Bekannte + Nur lokal + Ignoriert beobachtete Nachrichten aus fremden Netzen, die offen sind oder die, die nicht entschlüsselt werden können. Sendet nur die Nachricht auf den Knoten lokalen primären / sekundären Kanälen. + Keins + Nur für SENSOR, TRACKER und TAK_TRACKER zulässig. Verhindert alle Übertragungen, nicht anders als CLIENT_MUTE Rolle. + Geräterolle + Client + Client Base + Mit der App verbundenes oder eigenständiges Nachrichtengerät. + Client - Versteckt + Gerät, das nur bei Bedarf sendet, um nicht entdeckt zu werden oder Strom zu sparen. + Client Mute + Gerät, das keine Pakete von anderen Geräten weiterleitet. + Tracker + Sendet den Standort regelmäßig als Nachricht an den Standardkanal, um bei der Wiederherstellung des Geräts zu helfen. + Repeater + Router + Router Client + Router - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Dieser Knoten wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus. + Router mit Verzögerung + Sensor + Telemetrie-Pakete mit Priorität gesendet. + TAK + Optimiert für ATAK-Systemkommunikation, verringert die Anzahl der Routineübertragungen. + TAK Tracker + Aktiviert automatische TAK-PLI-Übertragungen und verringert die Anzahl der Routineübertragungen. + Tracker + GPS-Positionspakete mit Priorität gesendet. + Zeitzone + Karussellintervall + Wechselt automatisch wie ein Karussell zur nächsten Seite auf dem Bildschirm, basierend auf dem angegebenen Intervall. + Immer nach Norden zeigen + Die Kompassrichtung auf dem Bildschirm außerhalb des Kreises zeigt immer nach Norden. + Kompassausrichtung + Anzeigemodus + Standardlayout überschreiben. + Angelsächsisch + Metrisch + Bildschirm spiegeln + Anzeige vertikal spiegeln. + Fette Überschrift + Überschriftentext fett darstellen. + OLED Typ + Automatische OLED Erkennung überschreiben. + Bildschirm eingeschaltet für + Wie lange die Anzeige eingeschaltet bleibt, nachdem die Benutzertaste gedrückt oder Nachrichten empfangen wurden. + Anzeigeeinheiten + Einheiten die angezeigt werden. + 12h Uhrformat verwenden + Wenn aktiviert, zeigt das Gerät die Uhrzeit im 12-Stunden-Format auf dem Bildschirm an. + Aufwachen durch Tippen oder Bewegung + Erfordert, dass Ihr Gerät über einen Beschleunigungsmesser verfügt. + Ausgabe LED aktiv hoch + Alarmglocke LED + Alarmglocke Summer + Alarmglocke Vibration + Warnmeldung LED + Warnmeldung Summer + Warnmeldung Vibration + Externe Benachrichtigungen aktiviert + Nervige Verzögerung + Ausgabe LED (GPIO) + Ausgabe Summer (GPIO) + Ausgabe Vibration (GPIO) + I2S als Buzzer verwenden + Benutze PWM Summer + Bandbreite + Frequenzschlitz + Die Betriebsfrequenz Ihres Knotens wird basierend auf der Region, dem Modem Preset und diesem Feld berechnet. Wenn "0", wird der Slot automatisch auf der Grundlage des primären Kanalnamens berechnet. Wenn ein privater als Primär- und öffentliche als Sekundärkanäle konfiguriert sind, kann es sein, dass sie keinen Empfang auf den öffentlichen haben, wenn diese sich auf einem anderen Slot befinden. Stellen Sie in diesem Fall den richtigen Slot fest ein, statt die Automatik (0) übernehmen zu lassen. + Fehlerkorrektur + OK für MQTT + Anzahl der Weiterleitungen + Legt die maximale Sprungweite fest. Der Standardwert ist 3. Eine Erhöhung der Sprungweite erhöht auch die Überlastung und sollte daher mit Vorsicht verwendet werden. Nachrichten mit Sprungweite 0 erhalten keine Bestätigung. + MQTT ignorieren + Voreinstellungen + Leicht - Schnell + Leicht - Langsam + Hohe Reichweite - Schnell + Hohe Reichweite - Mäßig + Hohe Reichweite - Langsam + Hohe Reichweite - Turbo + Mittlere Reichweite - Schnell + Mittlere Reichweite - Langsam + Schmal - Schnell + Schmal - Langsam + Kurze Reichweite - Schnell + Kurze Reichweite - Langsam + Kurze Reichweite - Turbo + Sehr hohe Reichweite - Langsam + Duty-Cycle überschreiben + Frequenz überschreiben + PA Fan deaktiviert + Region + Die Region, in der Sie das Funkgerät benutzen. + Brasilien 902 MHz + China + Europäische Union 433 MHz + Europäische Union 868 MHz + Indien + Japan + Korea + Malaysia 433 MHz + Malaysia 919 MHz + Nepal 865 MHz + Neuseeland 865 MHz + Philippinen 433 MHz + Philippinen 868 MHz + Philippinen 915 MHz + Russland + Singapur 923 MHz + Thailand + Taiwan + Ukraine 433 MHz + Ukraine 868 MHz + Vereinigte Staaten von Amerika + Spreizfaktor + Empfangsverstärkung + Senden aktiviert + Sendeleistung + Voreinstellung verwenden + Aufklärer + Sanitäter + Scharfschütze + Teamleiter + Teammitglied + Nachricht + Adresse + MQTT aktiviert + Verschlüsselung aktiviert + Ihr Knoten sendet in regelmäßigen Abständen eine unverschlüsselte Nachricht mit Kartenbericht an den konfigurierten MQTT-Server. Einschließlich ID, langen und kurzen Namen, ungefährer Standort, Hardwaremodell, Geräterolle, Firmware-Version, LoRa Region, Modem-Voreinstellung und Name des Primärkanal. + Passwort + Proxy zu Client aktiviert + Hauptthema + TLS aktiviert + Benutzername + Nachbarinformationen aktiviert + Übertragen über LoRa + Ob unsere Nachbarinformation zusätzlich zum Senden an MQTT und die Phone-API auch über LoRa übertragen werden soll. Nicht verfügbar auf einem Kanal mit Standardschlüssel und -name. + GPS Abfrageintervall + IPv4 Modus + Aktivieren Sie die Übertragung von Paketen per UDP über das lokale Netzwerk. + Ethernet aktiviert + Durch Aktivieren von Ethernet wird die Bluetooth Verbindung zur App deaktiviert. TCP Knotenverbindungen sind auf Apple Geräten nicht verfügbar. + DNS + Gateway + IP + Subnetz + NTP Server + Keins + UDP Aussendung + rsyslog Server + WLAN aktiviert + Durch die Aktivierung von WLAN wird die Bluetooth Verbindung zur App deaktiviert. + Passwort + SSID + BLE RSSI Schwellenwert (Standard -80) + Besucherzähler aktiviert + GPS Abfrageintervall + Intelligente Entfernung + Die minimale Entfernungsänderung in Metern, die für eine intelligente Standortübermittlung berücksichtigt werden muss. + Intelligentes Intervall + Fester Standort + GPIO GPS aktiv + GPS-Chip (Hardware) Modus + GPS Abfrageintervall + Intervall zur Erfassung der Position (<10sek. = dauerhaft). + Deaktiviert + Aktiviert + Übertragungsintervall + Die maximale Verzögerung, ehe ein Knoten einen Standort erneut sendet. + Intelligente Position + Standort Optionen + Optionale Felder, die bei der Zusammenstellung von Standortnachrichten enthalten sein sollen. Je mehr Optionen ausgewählt werden, desto größer wird die Nachricht und die längere Übertragungszeit erhöht das Risiko für einen Nachrichtenverlust. + Höhe + Höhe in Bezug auf Meeresspiegel + DOP + Geoidale Höhentrennung + Fahrzeugsteuerkurs + Anzahl Satelliten + Sequenznummer + Fahrzeuggeschwindigkeit + Zeitstempel + GPIO GPS Empfangen + GPIO GPS Senden + ADC Multiplikator Überschreibungsverhältnis + Energiesparmodus aktivieren + Versetzt alles so weit wie möglich in den Ruhezustand. Für die Tracker- und Sensorfunktion umfasst dies auch das Lora Funkgerät. Verwenden Sie diese Einstellung nicht, wenn Sie Ihr Gerät mit den Telefon Apps verwenden möchten oder wenn Sie ein Gerät ohne Benutzertaste verwenden. + Herunterfahren bei Stromausfall + Zeit für Warten auf Bluetooth + Reichweitentest aktiviert + Speichere .CSV im Speicher (nur ESP32) + Sendeintervall + Administrativer Schlüssel + Der öffentliche Schlüssel, der zum Senden von administrativen Nachrichten an diesen Knoten berechtigt ist. + Debug-Protokoll-API aktiviert + Ausgabe von Echtzeit-Fehlersuchprotokollen über die serielle Schnittstelle, Anzeige und Export von positionskorrigierten Geräteprotokollen über Bluetooth. + Verwalteter Modus + Das Gerät wird von einem Netzwerkadministrator verwaltet, der Benutzer kann auf keine der Geräteeinstellungen zugreifen. + Privater Schlüssel + Wird verwendet, um einen gemeinsamen Schlüssel mit einem entfernten Gerät zu erstellen + Öffentlicher Schlüssel + Serielle Konsole + Serielle Konsole über die Stream-API. + Serielle Baudrate + Serielle Baudrate + Echo aktiviert + Wenn aktiviert, werden alle Nachrichten, die Sie senden, an Ihr Gerät zurückgesendet. + Serielle Schnittstelle aktiviert + Serieller Modus + Empfang + 38400 Baud + Standardwert + Standardwert + NMEA Positionen + Protocol Buffer + Einfach + Textnachricht + Zeitlimit erreicht + Senden + Speichern & Weiterleiten aktiviert + Puls + Verlauf Rückgabewert maximal + Zeitraum Rückgabewert + Server + Anzahl Einträge + Rolle + Team + Blau + Braun + Türkis + Dunkelblau + Dunkelgrün + Grün + Lila + Kastanienbraun + Orange + Violett + Rot + Blaugrün + Weiß + Gelb + Modul Luftqualität aktiviert + Aktualisierungsintervall der Luftqualität + Gerätetelemetrie senden + Aktivieren oder deaktivieren Sie die Gerätetelemetrie, um Metriken an das Netzwerk zu senden. Das Intervall ist ein Richtwert. Bei überlasteten Netzwerken werden die Intervalle automatisch anhand der Anzahl der Online Knoten verlängert. + Aktualisierungsintervall für Gerätedaten + Umweltdaten in Fahrenheit + Modul Umweltdaten aktiviert + Umweltdatenanzeige aktiviert + Aktualisierungsintervall für Umweltdaten + Modul Energiedaten aktiviert + Energiedatenanzeige aktiviert + Aktualisierungsintervall für Energiedaten + Unbekannter Paketgrenzwert + diff --git a/core/resources/src/commonMain/composeResources/values-de/strings.xml b/core/resources/src/commonMain/composeResources/values-de/strings.xml index 5f9b0f5175..1ac60832b2 100644 --- a/core/resources/src/commonMain/composeResources/values-de/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-de/strings.xml @@ -17,17 +17,18 @@ --> + Luftfeuchtigkeit %1$s: %2$s Nachricht von %1$s: %2$s %1$s Abwesend Favorit - %1$d Sprünge entfernt Zuletzt gehört %1$s Offline Online Rolle %1$s Signal %1$s Hop %1$d: %2$d Knoten + Temperatur Über Akzeptieren Danksagungen @@ -44,7 +45,6 @@ Nachricht übersetzen Aktionen ADC Multiplikationsfaktor - ADC Multiplikator Überschreibungsverhältnis Hinzufügen Persönliche Notiz hinzufügen. @@ -57,7 +57,6 @@ Gerät manuell hinzufügen… Netzwerkebene hinzufügen Adresse - Administrativer Schlüssel Administrativer Schlüssel Einstellung Fortgeschritten @@ -67,24 +66,14 @@ Luftqualität Symbol für Luftqualität Datenprotokoll Luftqualität - Modul Luftqualität aktiviert - Aktualisierungsintervall der Luftqualität Prozentuale Sendezeit für die Übertragung innerhalb der letzten Stunde. Sendezeit - - Alarmglocke Summer - Alarmglocke LED Warnklingelzeichen! - Alarmglocke Vibration - Warnmeldung Summer - Warnmeldung LED - Warnmeldung Vibration Alle Eingabequelle zulassen Erlaube undefinierten Pin-Zugriff Höhe Höhe - Immer nach Norden zeigen Umgebungslicht Umgebungsbeleuchtungseinstellungen Die Analysedaten helfen uns, die Android-App zu verbessern (Danke). Wir erhalten anonymisierte Informationen zum Nutzerverhalten. Dazu gehören Absturzberichte, in der App verwendete Bildschirme usw. @@ -130,20 +119,15 @@ Speichert die öffentlichen und privaten Schlüssel im sicheren, verschlüsseltem Speicher auf diesem Gerät. Sichern & Wiederherstellen Schlecht - - Bandbreite Luftdruck Akku Akku INA_2XX I2C Adresse Bluetooth Geräte - BLE RSSI Schwellenwert (Standard -80) - Blau Bluetooth Verfügbare Bluetooth Geräte Bluetooth Einstellungen Bluetooth ist aus. Schalten Sie es an, um nach Geräten in der Nähe zu suchen. - Bluetooth aktiviert Einstellungen Verwalten Sie drahtlos Ihre Geräteeinstellungen und Kanäle. Entdecken @@ -155,14 +139,10 @@ Bluetooth Scan Limit erreicht. Versuche es in %1$d Sekunde erneut. Bluetooth Scan Limit erreicht. Versuche es in %1$d Sekunden erneut. - Fette Überschrift Kopplung fehlgeschlagen. Erteilen Sie Geräten in der Nähe die Berechtigungen und versuchen Sie es erneut. Kopplung nicht abgeschlossen. Versuchen Sie es erneut. Einstellungen - Übertragungsintervall Rauschpegel - GPIO Taste - GPIO Summer Wird berechnet... Rufzeichen Kamera-Berechtigungen @@ -175,7 +155,6 @@ Vordefinierte Nachrichten aktiviert Konnte den Kanal nicht ändern, da das Funkgerät noch nicht verbunden ist. Bitte versuchen Sie es erneut. Herunterfahren wird auf diesem Gerät nicht unterstützt - Karussellintervall Auslastung für den aktuellen Kanal, einschließlich fehlerfreier TX, RX und fehlerhaftem RX (Rauschen). Kanal @@ -189,7 +168,6 @@ Kanal 8 Kanalfunktionen Diese Kanal-URL ist ungültig und kann nicht verwendet werden - Kanal Kanalname Kanal URL Kanalauslastung @@ -228,9 +206,6 @@ Schließen Auswahl schließen CO₂ - CODEC 2 aktiviert - CODEC2 Abtastrate - Fehlerkorrektur Diagramm ausblenden Einklappen Kommunizieren Sie außerhalb des Netzwerks mit Ihren Freunden und der Gemeinschaft ohne Mobilfunkdienst. @@ -243,46 +218,21 @@ Standortberechtigung ist erforderlich, um Entfernung und Kurs anzuzeigen. Dieses Gerät hat keinen Kompass-Sensor. Die Richtung ist nicht verfügbar. Kompass Norden oben - Kompassausrichtung Kompass Geschätzte Fläche: \u00b1%1$s (\u00b1%2$s) Geschätzte Fläche: unbekannte Genauigkeit Kompromittierte Schlüssel erkannt, wählen Sie OK, um diese neu zu erstellen. - Behandle doppeltes Antippen mit unterstützten Beschleunigungssensoren wie einen Benutzer-Tastendruck. Steuert die blinkende LED auf dem Gerät. Bei den meisten Geräten wird damit eine von bis zu 4 LEDs gesteuert, nicht jedoch die LEDS zum Laden und für das GPS. - Ob unsere Nachbarinformation zusätzlich zum Senden an MQTT und die Phone-API auch über LoRa übertragen werden soll. Nicht verfügbar auf einem Kanal mit Standardschlüssel und -name. Senden Sie den Standort auf dem primären Kanal, wenn dreimal auf die Benutzertaste gedrückt wird. Zeitzone für Daten auf dem Gerätebildschirm und Log. Zeitzone des Telefons verwenden - Wechselt automatisch wie ein Karussell zur nächsten Seite auf dem Bildschirm, basierend auf dem angegebenen Intervall. - Die Kompassrichtung auf dem Bildschirm außerhalb des Kreises zeigt immer nach Norden. - Standardlayout überschreiben. - Anzeige vertikal spiegeln. - Überschriftentext fett darstellen. - Automatische OLED Erkennung überschreiben. - Wie lange die Anzeige eingeschaltet bleibt, nachdem die Benutzertaste gedrückt oder Nachrichten empfangen wurden. - Einheiten die angezeigt werden. - Erfordert, dass Ihr Gerät über einen Beschleunigungsmesser verfügt. - Die Betriebsfrequenz Ihres Knotens wird basierend auf der Region, dem Modem Preset und diesem Feld berechnet. Wenn "0", wird der Slot automatisch auf der Grundlage des primären Kanalnamens berechnet. Wenn ein privater als Primär- und öffentliche als Sekundärkanäle konfiguriert sind, kann es sein, dass sie keinen Empfang auf den öffentlichen haben, wenn diese sich auf einem anderen Slot befinden. Stellen Sie in diesem Fall den richtigen Slot fest ein, statt die Automatik (0) übernehmen zu lassen. - Legt die maximale Sprungweite fest. Der Standardwert ist 3. Eine Erhöhung der Sprungweite erhöht auch die Überlastung und sollte daher mit Vorsicht verwendet werden. Nachrichten mit Sprungweite 0 erhalten keine Bestätigung. Verfügbare Modemvoreinstellungen, Standard ist LongFast. Die Region, in der Sie das Funkgerät benutzen. - Durch Aktivieren von Ethernet wird die Bluetooth Verbindung zur App deaktiviert. TCP Knotenverbindungen sind auf Apple Geräten nicht verfügbar. Aktivieren Sie die Übertragung von Paketen per UDP über das lokale Netzwerk. - Die maximale Verzögerung, ehe ein Knoten einen Standort erneut sendet. - Die minimale Entfernungsänderung in Metern, die für eine intelligente Standortübermittlung berücksichtigt werden muss. - Am schnellsten werden Standortaktualisierungen gesendet, wenn die Mindestentfernung eingehalten wurde. - Optionale Felder, die bei der Zusammenstellung von Standortnachrichten enthalten sein sollen. Je mehr Optionen ausgewählt werden, desto größer wird die Nachricht und die längere Übertragungszeit erhöht das Risiko für einen Nachrichtenverlust. - Intervall zur Erfassung der Position (<10sek. = dauerhaft). - Versetzt alles so weit wie möglich in den Ruhezustand. Für die Tracker- und Sensorfunktion umfasst dies auch das Lora Funkgerät. Verwenden Sie diese Einstellung nicht, wenn Sie Ihr Gerät mit den Telefon Apps verwenden möchten oder wenn Sie ein Gerät ohne Benutzertaste verwenden. Der Knoten wird neugestartet und wird kurz nicht erreichbar sein. - Der öffentliche Schlüssel, der zum Senden von administrativen Nachrichten an diesen Knoten berechtigt ist. - Ausgabe von Echtzeit-Fehlersuchprotokollen über die serielle Schnittstelle, Anzeige und Export von positionskorrigierten Geräteprotokollen über Bluetooth. - Das Gerät wird von einem Netzwerkadministrator verwaltet, der Benutzer kann auf keine der Geräteeinstellungen zugreifen. Wird verwendet, um einen gemeinsamen Schlüssel mit einem entfernten Gerät zu erstellen. Wird aus Ihrem privaten Schlüssel generiert und an andere Knoten im Netzwerk gesendet, damit diese einen gemeinsamen geheimen Schlüssel berechnen können. - Serielle Konsole über die Stream-API. Einstellungen Bluetooth Berechtigungen konfigurieren Kritische Warnungen konfigurieren @@ -330,7 +280,6 @@ Filter enthält Voreingestellte Filter Filter - Debug-Protokoll-API aktiviert Keine App logs zum Anzeigen Aktualisieren Protokolle exportieren @@ -375,8 +324,6 @@ Details Erkennungssensor Sensoreinstellungen für Erkennung - Erkennungssensor aktiviert - Typ der Erkennungsauslösung Gerät Geräteeinstellungen @@ -389,17 +336,15 @@ Gerätedaten %1$s %1$s: %2$s%% - Aktualisierungsintervall für Gerätedaten %1$s: %2$s V Gerät schläft Gerätespeicher & UI (schreibgeschützt) - Gerätetelemetrie senden - Aktivieren oder deaktivieren Sie die Gerätetelemetrie, um Metriken an das Netzwerk zu senden. Das Intervall ist ein Richtwert. Bei überlasteten Netzwerken werden die Intervalle automatisch anhand der Anzahl der Online Knoten verlängert. Design %1$s, Sprache %2$s Taupunkt Direktnachricht Schlüssel für direkte Nachrichten Direktnachrichten + Deaktiviert Verwerfen Verbindung trennen Verbindung getrennt @@ -456,22 +401,16 @@ Scan stoppen KI Analyse nicht verfügbar %1$s übrig - %1$d einzigartige Knoten Karte anschauen Laufwerkspeicher frei %1$d - Display Anzeige des Geräts - Anzeigemodus - Wenn aktiviert, zeigt das Gerät die Uhrzeit im 12-Stunden-Format auf dem Bildschirm an. - Anzeigeeinheiten Distanz Entfernungsfilter Filtern Sie die Knotenliste und die Mesh-Karte nach der Nähe zu Ihrem Telefon. Entfernungsmessungen Zeigt den Abstand zwischen Ihrem Telefon und anderen Meshtastic Knoten mit einem Standort an. - DNS Neue Suche Bluetooth, USB, TCP, Kopplung, seriell, WLAN @@ -506,6 +445,7 @@ MQTT Knoten Kenndaten Knoten + Benachrichtigungen Erste Schritte Einstellungen — Module & Administration Einstellungen — Radio & Benutzer @@ -516,38 +456,28 @@ Einheiten & Sprache Fertig Für dieses Gerät nicht erneut anzeigen - Doppelklick als Taste Nachrichten von einem öffentlichen Internet Gateway werden an das lokale Mesh weitergeleitet. Aufgrund der Nullsprungrichtlinie wird der Datenverkehr vom MQTT Standardserver nicht über dieses Gerät hinaus weitergeleitet. Herunterladen Doppelter öffentlicher Schlüssel erkannt + Dynamisch Leicht einzurichtende, private Netznetze für sichere und zuverlässige Kommunikation in entlegenen Gebieten. - Echo aktiviert Bearbeiten Netzwerk Kachelquelle bearbeiten 8 Stunden - Energiesparmodus aktivieren Aktiviert - - Verschlüsselung aktiviert Öffentlicher Schlüssel stimmt nicht überein Der öffentliche Schlüssel stimmt nicht mit dem gespeicherten Schlüssel überein. Sie können den Knoten entfernen und den Schlüsselaustausch erneut durchführen lassen. Dies könnte jedoch auf ein schwerwiegenderes Sicherheitsproblem hindeuten. Kontaktieren Sie den Benutzer über einen anderen vertrauenswürdigen Kanal, um zu klären, ob die Schlüsseländerung auf ein Zurücksetzen auf Werkseinstellungen oder eine andere absichtliche Handlung zurückzuführen ist. Verschlüsselung mit öffentlichen Schlüssel Umweltdaten - Umgebung - Modul Umweltdaten aktiviert - Umweltdatenanzeige aktiviert - Aktualisierungsintervall für Umweltdaten - Umweltdaten in Fahrenheit Fehler Limit für den aktuellen Betriebszyklus erreicht. Nachrichten können momentan nicht gesendet werden, bitte versuchen Sie es später erneut. Konnte nach mehreren Versuchen keine stabile Verbindung herstellen. Bitte wähle den Knoten erneut aus oder versuche es erneut. Verbinden & Verwalten Fernverwaltung wird aufgebaut Ethernet Einstellungen - Ethernet aktiviert Ethernet IP: Standort austauschen Diagramm einblenden @@ -560,7 +490,6 @@ TAK Datenpaket exportieren Externe Benachrichtigung Einstellungen für externe Benachrichtigungen - Externe Benachrichtigungen aktiviert Auf Werkseinstellungen zurücksetzen Angemessen Meshtastic %1$s @@ -571,10 +500,6 @@ Verfügbare Dateien (%1$d): Wort oder Regex Muster hinzufügen - Filter deaktivieren - Filter aktivieren - Filter aktivieren - Nachrichten ausblenden, die Filterwörter enthalten %1$d gefilterte ausblenden Filter Gefiltert @@ -653,6 +578,7 @@ Dies könnte einige Minuten dauern... Zielversion: %1$s Firmware Aktualisierung + %1$d% Unbekannter Fehler Unbekanntes Hardware Modell: %1$d Unbekannte Version @@ -669,21 +595,13 @@ Warte auf den Neustart des Geräts in den OTA Modus... Warte auf eine erneute Verbindung... Firmware Version: %1$s - Festgelegter Pin Fester Standort - Bildschirm spiegeln Weitere Informationen finden Sie in unserer Datenschutzrichtlinie. Freier Speicher Verfügbarer Systemspeicher in Bytes Frequenz - Frequenzschlitz - Anzeigename Gaswiderstand - Gateway - Eingabeereignis bei Gegenuhrzeigersinn generieren - Eingabeereignis bei Uhrzeigersinn generieren - Eingabeereignis beim Drücken generieren QR Code Erzeugen %1$s hat %2$s betreten @@ -694,28 +612,14 @@ Aus Erste Schritte Gut - GPIO GPIO Pin - GPIO-Pin für Drehencoder A Port - GPIO-Pin für Drehencoder B Port - GPIO-Pin für Drehencoder Knopf Port - Zu überwachender GPIO-Pin - GPIO GPS aktiv - GPS-Chip (Hardware) Modus - GPIO GPS Empfangen - GPIO GPS Senden - Grün Hardware Geräte-Modell Überschrift - Puls Hilfe & Dokumentation Ebene ausblenden Passwort verbergen - Verlauf Rückgabewert maximal - Zeitraum Rückgabewert - Anzahl der Weiterleitungen Sprünge entfernt Host Host Kennzahlen @@ -723,18 +627,12 @@ Ich stimme zu. Ich habe das oben stehende gelesen und verstanden. Ich willige ein in die unverschlüsselte Übertragung der Daten meines Nodes. Ich weiß was ich tue. - I2S Takt - I2S Daten Eingang - I2S Daten Ausgang - I2S Wortauswahl IAQ (Innenluftqualität) relativer IAQ-Wert gemessen von Bosch BME680. Symbolbedeutung - Ignorieren '%1$s' zur Ignorieren-Liste hinzufügen? Eingehende ignorieren - MQTT ignorieren '%1$s' von der Ignorieren-Liste entfernen? Einstellungen importieren @@ -756,7 +654,6 @@ IP IP-Adresse: Port: - IPv4 Modus JSON-Ausgabe aktiviert %1$s @@ -773,8 +670,6 @@ Netzwerkschicht Mehr erfahren - Puls LED - LED Zustand Veralteter administrativer Kanal %1$d Bibliotheken @@ -851,11 +746,9 @@ Lux Benutzerdefinierte Kachelquellen verwalten Kartenebenen verwalten - Verwalteter Modus Manuelle Standortanfrage erforderlich Mesh Karte - Zwischenspeichergröße: %1$d MB\nZwischenspeicherverwendung: %2$d MB Zwischenspeicher-Verwaltung Aktuelle Zwischenspeichergröße %1$d Kacheln @@ -873,11 +766,8 @@ Offline-Verwaltung Das Säubern des SQL-Zwischenspeichers ist fehlgeschlagen, siehe Logcat für Details SQL-Zwischenspeicher gelöscht für %1$s - Kartenberichte Einwilligung zum Teilen von unverschlüsselten Node-Daten über MQTT Indem Sie diese Funktion aktivieren, erklären Sie sich ausdrücklich Einverstanden mit der Übertragung des geographischen Standorts Ihres Gerätes in Echtzeit über das MQTT-Protokoll und ohne Verschlüsselung. Diese Standortdaten können zum Beispiel für Live-Kartenerichte, Geräteverfolgung und zugehörige Telemetriefunktionen verwendet werden. - Kartenberichtsintervall (Sekunden) - Ihr Knoten sendet in regelmäßigen Abständen eine unverschlüsselte Nachricht mit Kartenbericht an den konfigurierten MQTT-Server. Einschließlich ID, langen und kurzen Namen, ungefährer Standort, Hardwaremodell, Geräterolle, Firmware-Version, LoRa Region, Modem-Voreinstellung und Name des Primärkanal. Region zum Herunterladen auswählen Herunterladen starten Auswahl Kartenstil @@ -948,9 +838,6 @@ Nachrichten µg/m³ Minimum - Minimale Übertragungszeit (Sekunden) - Intelligente Entfernung - Intelligentes Intervall Minimale Aufwachzeit Voreinstellungen Moduleinstellungen @@ -960,7 +847,6 @@ MQTT MQTT Einstellungen - MQTT aktiviert MQTT: Verbindung verloren MQTT Proxy fehlgeschlagen: %1$s MQTT: Verbindung abgelehnt: %1$s @@ -973,8 +859,6 @@ Broker (TCP) nicht erreichbar Zeitüberschreitung nach %1$d ms TLS Aushandlung fehlgeschlagen: %1$s - MQTT Proxy auf diesem Handy - Dieses Telefon leitet den MQTT-Datenverkehr für das verbundene Gerät weiter. Deaktivieren Sie diese Funktion, um die Weiterleitung sofort zu unterbrechen, ohne die MQTT-Einstellungen des Geräts zu ändern – dies ist nützlich, wenn der MQTT-Datenverkehr die Verbindung überlastet. Aktivieren Sie sie wieder, um die Weiterleitung fortzusetzen. Verbunden Wird verbunden… Verbindung getrennt @@ -997,7 +881,6 @@ Stumm für %1$d Tage, %2$s Stunden Stumm für %1$s Stunden Nicht stumm - Nervige Verzögerung (Sekunden) Name Name darf nicht leer sein. Zurück navigieren @@ -1005,7 +888,6 @@ Benötigen Sie Hardware? Nachbarinformation Einstellungen Nachbarinformation - Nachbarinformationen aktiviert Netzwerk Neue Kanal-URL empfangen Neue Nachrichten unten @@ -1018,7 +900,6 @@ Keine Bluetoothgeräte gefunden Keine eigenen Kachelquellen gefunden. Kein Gerät ausgewählt - Keine Geräte gefunden Keine Dateien vorhanden. Keine Statistiken verfügbar Keine Kartenebenen geladen. @@ -1076,7 +957,6 @@ über Favorit über MQTT Node-Datenbank zurücksetzen - Knoteninfo Übertragungsintervall Knoten Knoten an diesem Standort @@ -1094,6 +974,8 @@ Nicht verbunden Anmerkung Knoten + + Netz Meshtastic nutzt Benachrichtigungen, um Sie über neue Nachrichten und andere wichtige Ereignisse auf dem Laufenden zu halten. Sie können Ihre Benachrichtigungsrechte jederzeit in den Einstellungen aktualisieren. Benachrichtigungen für Kanal und Direktnachrichten. @@ -1102,13 +984,9 @@ Benachrichtigungen für erhaltene Alarmglocke Benachrichtigungen für Empfangsbestätigung Jetzt - NTP Server - Anzahl Einträge Offline Karten - OK für MQTT OK - OLED Typ 24 Stunden 1 Stunde @@ -1126,25 +1004,14 @@ Öffne Wlan-Einstellungen Optionen Ausrichtung Nord - - Ausgabe Summer (GPIO) - Ausgabedauer (GPIO) - Ausgabe LED aktiv hoch - Ausgabe LED (GPIO) - Ausgabe Vibration (GPIO) Überlaufmenü Seriellen Anschluss der Konsole überschreiben - Duty-Cycle überschreiben - Frequenz überschreiben - PA Fan deaktiviert Paket Authentizität Empfohlen. Lehne unsignierte downgrade-Versuche von Knoten ab, von denen bekannt ist, dass sie signieren. - Authentifiziere Pakete, wenn möglich, aber akzeptiere unsignierten Verkehr für maximale Kompatibilität. Schutzstufe Zeige und verarbeite nur kryptografisch authentisierte Mesh-Pakete. Ältere Knoten und übergroße Pakete könnten verschwinden. Dieses verbundene Gerät unterstützt keine Überprüfung der Paketsignatur. - Kopplungsmodus Passwort Besucher @@ -1156,7 +1023,6 @@ W:%1$d Besucherzähler Einstellung Besucherzähler - Besucherzähler aktiviert Regelmäßige Standortübertragung Meshtastic benötigt die Berechtigung „Geräte in der Nähe“, um Geräte über Bluetooth zu finden und eine Verbindung zu ihnen herzustellen. Sie können die Funktion deaktivieren, wenn sie nicht verwendet wird. @@ -1177,6 +1043,7 @@ %1$d Sekunde %1$d Sekunden + PM1.0 PM10 PM2.5 @@ -1184,16 +1051,11 @@ Standort Vom aktuellen Telefonstandort festlegen Standort aktiviert - Standort Optionen Standort Standortnachricht - Energie Energie Einstellungen Energiedaten - Modul Energiedaten aktiviert - Energiedatenanzeige aktiviert - Aktualisierungsintervall für Energiedaten Angeschaltet ppm Genauer Standort @@ -1215,12 +1077,9 @@ Text Primär Regelmäßiges senden von Standort und Telemetrie - Privater Schlüssel Standort zum Mesh angeben Der Name des Anbieters existiert bereits. - Proxy zu Client aktiviert PSK - GPIO Pin PTT Öffentlicher Schlüssel Öffentlicher Schlüssel geändert QR-Code @@ -1238,25 +1097,14 @@ Regen (24 Std.) Reichweitentest Einstellungen Reichweitentest - Reichweitentest aktiviert Reagieren Neustarten - - Weiterleitungsmodus - Sende jede empfangene Nachricht erneut aus, egal ob sie auf einem privaten Kanal oder von einem anderen Mesh mit den gleichen LoRa Parametern stammt. - Das gleiche Verhalten wie ALLE aber überspringt die Paketdekodierung und sendet sie einfach erneut. Nur in Repeater Rolle verfügbar. Wenn Sie diese auf jede andere Rolle setzen, wird ALLE Verhaltensweisen folgen. - Ignoriert Nachrichten von nicht standardmäßigen Anschlussnummern wie: TAK, Range Test, Besucherzähler, etc. Sendet nur Nachrichten wie: Knoteninfo, Text, Standort, Telemetrie und Weiterleitung erneut. - Ignoriert beobachtete Nachrichten von fremden Meshes wie bei LOCAL ONLY, geht jedoch einen Schritt weiter, indem auch Nachrichten von Nodes ignoriert werden, die nicht bereits in der bekannten Liste der Nodes enthalten sind. - Ignoriert beobachtete Nachrichten aus fremden Netzen, die offen sind oder die, die nicht entschlüsselt werden können. Sendet nur die Nachricht auf den Knoten lokalen primären / sekundären Kanälen. - Nur für SENSOR, TRACKER und TAK_TRACKER zulässig. Verhindert alle Übertragungen, nicht anders als CLIENT_MUTE Rolle. Kürzliche Netzwerkgeräte Erneut verbinden… - Rot Aktualisieren Metadaten aktualisieren Sind Sie sicher, dass Sie den privaten Schlüssel neu erstellen möchten?\n\nAndere Knoten, die bereits Schlüssel mit diesem Knoten ausgetauscht haben, müssen diesen entfernen und erneut austauschen, um eine sichere Kommunikation fortzusetzen. Privaten Schlüssel neu erstellen - Region Höre %1$d Relais Höre %1$d Relais @@ -1306,32 +1154,17 @@ Geräterolle Client Client Base - Pakete von oder zu favorisierten Knoten werden als ROUTER_LATE weitergeleitet und alle anderen Pakete als CLIENT. - Mit der App verbundenes oder eigenständiges Messaging-Gerät. Client - Versteckt - Gerät, das nur bei Bedarf sendet, um nicht entdeckt zu werden oder Strom zu sparen. Client Mute - Gerät, das keine Pakete von anderen Geräten weiterleitet. Tracker - Sendet den Standort regelmäßig als Nachricht an den Standardkanal, um die Gerätewiederherstellung zu erleichtern. Repeater - Infrastrukturknoten zur Erweiterung der Netzabdeckung durch Weiterleitung von Nachrichten mit minimalem Overhead. In der Knotenliste nicht sichtbar. Router Router Client - Kombination von ROUTER und CLIENT. Nicht für mobile Endgeräte. - Knoten zur Erweiterung der Netzabdeckung durch Weiterleiten von Nachrichten. In Knotenliste sichtbar. Router mit Verzögerung - Infrastruktur-Node, der Pakete immer einmal erneut sendet, jedoch erst, nachdem alle anderen Modi durchlaufen wurden, um zusätzliche Abdeckung für lokale Cluster sicherzustellen. Sichtbar in der Node-Liste. Sensor - Telemetrienachricht mit Priorität gesendet. TAK - Optimiert für ATAK-Systemkommunikation, verringert die Anzahl der Routineübertragungen. TAK Tracker - Aktiviert automatische TAK-PLI-Übertragungen und verringert die Anzahl der Routineübertragungen. Tracker - GPS Standortnachricht mit Priorität gesendet. - Hauptthema - Drehencoder #1 aktiviert Ich habe die <a href="https://meshtastic.org/docs/configuration/radio/device/#roles"> Dokumentation der Geräterollen </a> und den dazugehörigen Blogeintrag <a href="http://meshtastic.org/blog/choosing-the-right-device-role"> über die Auswahl der richtigen Geräterolle</a> gelesen. negative Bestätigung erhalten @@ -1344,12 +1177,9 @@ Nachricht ist zu groß zum Senden RSSI Indikator für die empfangene Signalstärke, eine Messung zur Bestimmung der von der Antenne empfangenen Leistungsstärke. Ein höherer RSSI-Wert weist im Allgemeinen auf eine stärkere und stabilere Verbindung hin. - rsyslog Server Satelliten - Speichern Speichern - Speichere .CSV im Speicher (nur ESP32) Reichweitentest exportieren Suchen @@ -1361,7 +1191,6 @@ Geteilten QR-Code scannen Suche... Suche... - Bildschirm eingeschaltet für Zum Ende springen Emojis suchen... Sekundär @@ -1391,19 +1220,8 @@ Ausgewählt Ausgewählter Kartentyp Senden - Glocke senden - Glocke mit Warnmeldung senden - Sendernachrichtenintervall (Sekunden) - Seriell - Serielle Baudrate Einstellungen serielle Schnittstelle - Serielle Konsole - Serielle Schnittstelle aktiviert - Serieller Modus - Empfang - Senden - Server Sitzung aktiv Aktualisierung erforderlich Verbindung einrichten @@ -1430,7 +1248,6 @@ Zeige Wegpunkte Herunterfahren Knoten: %1$s - Herunterfahren bei Stromausfall ⚠️ Dies wird den Knoten ausschalten. Eine physische Interaktion ist nötig, um ihn wieder einzuschalten. Signal Signalqualität @@ -1456,7 +1273,6 @@ Nutze derzeitige Knoten-Position Überspringen Position - Intelligente Position SNR Signal-Rausch-Verhältnis, ein in der Kommunikation verwendetes Maß, um den Pegel eines gewünschten Signals im Verhältnis zum Pegel des Hintergrundrauschens zu quantifizieren. Bei Meshtastic und anderen drahtlosen Systemen weist ein höheres SNR auf ein klareres Signal hin, das die Zuverlässigkeit und Qualität der Datenübertragung verbessern kann. Bodenfeuchte @@ -1464,16 +1280,11 @@ Geschwindigkeit %1$d km/h %1$d mph - Spreizfaktor - SSID - Statusübertragung (Sekunden) Statusmeldung Bleibe überall in Verbindung Verbindung trennen Speichern & Weiterleiten Speichern & Weiterleiten Einstellungen - Speichern & Weiterleiten aktiviert - Subnetz Erfolgreich Dauer Supertiefschlaf Unterstützt @@ -1481,21 +1292,10 @@ Löschen Stummschalten Stummschaltung aufheben - Empfangsverstärkung Systemeinstellungen TAK (ATAK) TAK Konfiguration - Mitgliedsrolle - Aufklärer - Hauptquartier - Hundeführer (K9) - Sanitäter - Funker - Scharfschütze - Teamleiter - Teammitglied - Unspecified TAK Server Lokalen TAK Server aktivieren … @@ -1508,22 +1308,6 @@ ✗ Ausführen Ausführen: %1$s - Teamfarbe - Blau - Braun - Türkis - Dunkelblau - Dunkelgrün - Grün - Lila - Kastanienbraun - Orange - Violett - Rot - Blaugrün - Unspecified - Weiß - Gelb Telemetrie Telemetrie Einstellungen Temperatur @@ -1532,10 +1316,8 @@ Hell System Zeit - Zeitzone Zeitlimit erreicht Zeitstempel - TLS aktiviert Standort einschalten Route verfolgen @@ -1582,7 +1364,6 @@ Übersetzungsmodell herunterladen? Nachricht konnte nicht übersetzt werden Nachricht ist schon in deiner Sprache - Übertragen über LoRa Transport API @@ -1596,11 +1377,11 @@ 24H 48 Stunden 2 Wochen - Senden aktiviert - Sendeleistung Typ Eine Nachricht schreiben UDP Übertragung + dBm + m System Angelsächsisch @@ -1618,9 +1399,6 @@ Auswahl aktivieren Unbekannt Nicht gesetzt - 0 - Up/Down/Select Eingang aktiviert - GPS Abfrageintervall - Aktualisierungsintervall (Sekunden) Aktualisiert Wenn aktiviert, werden Nachrichten aus dem Mesh über das konfigurierte Gateway eines beliebigen Knotens an das **öffentliche** Internet gesendet. Laufzeit @@ -1631,13 +1409,7 @@ URL Vorlage USB USB-Berechtigung verweigert. Verbinde das Gerät erneut, um es nochmal zu versuchen. - - 12h Uhrformat verwenden Kompakte Kodierung für Kyrillisch - I2S als Buzzer verwenden - Eingang PULLUP Einstellung - Voreinstellung verwenden - Benutze PWM Summer Benutzer Benutzer Einstellungen @@ -1645,7 +1417,6 @@ Benutzerinfo Benutzerzeichenkette Benutzerinfo - Benutzername UV Lux über API über MQTT @@ -1653,8 +1424,6 @@ Auf der Karte anzeigen Version ansehen Spannung - Zeit für Warten auf Bluetooth - Aufwachen durch Tippen oder Bewegung Warnung Wegpunkt löschen? Wegpunkt bearbeiten diff --git a/core/resources/src/commonMain/composeResources/values-el/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-el/schema_strings.xml new file mode 100644 index 0000000000..d2e783384b --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-el/schema_strings.xml @@ -0,0 +1,41 @@ + + + + + Μπλε + Πράσινο + Κόκκινο + Ακύρωση + Ονομα + Πελάτης Βάση + Περιφέρεια + Μήνυμα + Διεύθυνση + Κωδικός πρόσβασης + Όνομα χρήστη + IP + Κωδικός πρόσβασης + SSID + Αποθήκευση + Ιδιωτικό Κλειδί + Δημόσιο Κλειδί + Λήξη χρονικού ορίου + Μπλε + Πράσινο + Κόκκινο + diff --git a/core/resources/src/commonMain/composeResources/values-el/strings.xml b/core/resources/src/commonMain/composeResources/values-el/strings.xml index e800de1e3d..f897af7b94 100644 --- a/core/resources/src/commonMain/composeResources/values-el/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-el/strings.xml @@ -17,6 +17,8 @@ --> + Υγρασία + Θερμοκρασία Σχετικά Αποδοχή @@ -26,16 +28,13 @@ Διεύθυνση Διαχείριση - Εφαρμογή πολύ παλαιά Εφαρμογή Είστε σίγουροι ότι θέλετε να αλλάξετε στο προεπιλεγμένο κανάλι; Ήχος - Μπαταρία - Μπλε Bluetooth Ρυθμίσεις Bluetooth @@ -54,7 +53,6 @@ Κανάλι 7 Κανάλι 8 Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί - Κανάλι Όνομα Καναλιού Κανάλια @@ -88,19 +86,17 @@ Αποσυνδεδεμένο Μηνύματα - Οθόνη Απόσταση MQTT + Επεξεργασία 8 Ώρες - Ασυμφωνία δημόσιου κλειδιού Κρυπτογράφηση Δημόσιου Κλειδιού - Σφάλμα Επαναφορά εργοστασιακών ρυθμίσεων @@ -113,10 +109,7 @@ Αποτυχία ενημέρωσης - - Πράσινο Ξέρω τι κάνω. - Παράβλεψη Πληροφορίες @@ -141,7 +134,6 @@ LoRa LoRa - Χωρητικότητα προσωρινής μνήμης: %1$d MB\nΧρήση προσωρινής μνήμης: %2$d MB Διαχείριση Προσωρινής Αποθήκευσης Μέγεθος τρέχουσας προσωρινής μνήμης Η λήψη ολοκληρώθηκε! @@ -188,26 +180,25 @@ Διαγραφή Κανένα (απενεργοποιημένο) Αποσυνδεδεμένο + Εντάξει 24 Ώρες - Κωδικός πρόσβασης + Τοποθεσία Τοποθεσία - Γλώσσα Προκαθορισμένο του συστήματος - Ιδιωτικό Κλειδί Παρέχετε τοποθεσία στο πλέγμα PSK Δημόσιο Κλειδί @@ -219,9 +210,6 @@ Νέα γρήγορη συνομιλία Ρυθμίσεις συσκευής Επανεκκίνηση - - Κόκκινο - Περιφέρεια Απομακρυσμένη Διαχείριση @@ -238,7 +226,6 @@ Πελάτης Βάση Λήξη χρονικού ορίου - Αποθήκευση Αποθήκευση @@ -247,7 +234,6 @@ Ασφάλεια Επιλογή όλων Αποστολή - ρυθμίσεις Κοινοποίηση @@ -255,13 +241,9 @@ Τερματισμός λειτουργίας Οθόνη - SSID Διαγραφή Σίγαση - Μπλε - Πράσινο - Κόκκινο Τηλεμετρία Θέμα Σκούρο @@ -283,10 +265,8 @@ Άγνωστο Όνομα Χρήστη URL - Χρήστης - Όνομα χρήστη μέσω MQTT Τάση Διαγραφή σημείου πορείας; diff --git a/core/resources/src/commonMain/composeResources/values-es/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-es/schema_strings.xml new file mode 100644 index 0000000000..08e21fb4d8 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-es/schema_strings.xml @@ -0,0 +1,244 @@ + + + + + Azul + Intensidad + Verde + Estado de led + Rojo + Por defecto + Frecuencia de Muestreo CODEC2 + CODEC 2 activado + Salida de datos I2S + Reloj del I2S + Entrada de datos I2S + Selecionar palabras I2S + Pin PTT + Bluetooth activado + Modo de emparejamiento + Hacer una acción al girar en sentido contrario a las agujas del reloj + Hacer una acción al girar en sentido de las agujas del reloj + Hacer una acción al presionar + Pin A GPIO del Encoder + Pin B GPIO del Encoder + Pin de la Pulsación del Encoder + Cancelar + Ninguna + Encoder Número 1 Activado + Mandar campana 🔔 + Arriba/Abajo/Seleccionar Activado + Tipo de detección para activar + Sensor detector activado + Pin GPIO para monitorizar + Mote + Mandar la campana con el mensaje de alerta + Utilizar el modo de entrada PULL_UP + Botón GPIO + Zumbador GPIO + Deshabilitar clic triple + Doble pulsación como botón + Trate un doble toque en acelerómetros soportados como una pulsación de botón de usuario. + Latido LED + Intervalo de transmisión de información del nodo + Modo de retransmisión + Todos + Igual al comportamiento que TODOS pero omite la decodificación de paquetes y simplemente los retransmite. Sólo disponible en el rol repetidor. Establecer esto en cualquier otro rol dará como resultado TODOS los comportamientos. + Solo conocido + Solo locales + Ignora mensajes observados desde mallas foráneas que están abiertas o que no pueden descifrar. Solo retransmite mensajes en los nodos locales principales / canales secundarios. + Ninguna + Solo permitido para los roles SENSOR, TRACKER y TAK_TRACKER, esto inhibirá todas las retransmisiones, no a diferencia del rol de CLIENT_MUTE. + Rol del dispositivo + Cliente + Base cliente + Cliente oculto + Dispositivo que solo emite según sea necesario por sigilo o para ahorrar energía. + Cliente silenciado + El dispositivo no reenvía mensajes de otros dispositivos. + Perdido y encontrado + Transmite regularmente la ubicación como mensaje al canal predeterminado para asistir en la recuperación del dispositivo. + Repetidor + Router + Cliente de router + Sensor + Transmite paquetes de telemetría como prioridad. + TAK + Optimizado para el sistema de comunicación ATAK, reduciendo las transmisiones rutinarias. + Rastreador TAK + Permite la transmisión automática TAK PLI y reduce las transmisiones rutinarias. + Rastreador + Transmisión de paquetes de posición GPS como prioridad. + Zona horaria + Intervalo de carrusel + Cambia automáticamente a la siguiente página en la pantalla como un carrusel, basado en el intervalo especificado. + Siempre apuntar al norte + La dirección de la brújula en la pantalla, fuera del círculo, siempre apuntará hacia el norte. + Orientación de la brújula + Modo de la pantalla + Anula el diseño de pantalla predeterminado. + Métrico + Girar la pantalla 180º + Voltear la pantalla verticalmente. + Encabezado en negrita + Encabezado del texto de la pantalla en negrita. + Tipo de OLED + Anular detección automática de pantalla OLED. + Pantalla activa durante + Cuánto tiempo permanece encendido la pantalla después de pulsar el botón de usuario o recibir mensajes. + Unidades en pantalla + Unidades mostradas en la pantalla del dispositivo. + Utilizar el formato de 12h para el reloj + Si activado, el dispositivo mostrará la hora en la pantalla en el formato de 12h + Despertar al tocar o al mover + Requiere que haya un acelerómetro en su dispositivo. + LED de salida activo en alto + Notificación LED al recibir una campana + Notificación con el zumbador al recibir una campana + Notificación con vibración al recibir una campana + Notificación LED al recibir un mensaje + Notificación con el buzzer al recibir un mensaje + Notificación con vibración al recibir un mensaje + Notificaciones externas activadas + Salida LED (pin GPIO) + Salida buzzer (pin GPIO) + Salida vibratoria (pin GPIO) + Utilizar el Buzzer como uno I2S + Utilizar buzzer PWM + Ancho de Banda + Slot de frecuencia + La frecuencia de funcionamiento de su nodo se calcula en base a la región, el preajuste del módem y este campo. Cuando es 0, la ranura se calcula automáticamente basándose en el nombre del canal principal y cambiará de la rama pública predeterminada. Cambie a la ranura pública por defecto si se configuran los canales privados primarios y públicos secundarios. + Tasa de codificación + Permitir MQTT + Número de saltos + Establece el número máximo de saltos, por defecto 3. Aumentar saltos también incrementa la congestión y debe ser utilizado con cuidado. 0 saltos de difusión no obtendrán ACKs. + Ignorar Paquetes MQTT + Predefinidos + Sobreescribir el Tiempo de Trabajo + Sobreescribir frecuencia + Ventilador del Amplificador apagado + Región + La región donde utilizará su radio. + China + India + Japón + Taiwan + Estados Unidos + Factor de dispersión + Aumentar ganancia de RX + Transmisión Activa + Potencia de transmisión + Usar predefinido + Mensaje + Dirección del Servidor MQTT + Activar el MQTT + Permitir Encripción + Contraseña + Compartir Internet a la Radio + Tema raíz + Usuario + Información de Vecinos + Transmitir en LoRa + Si, además de enviarlo a MQTT y a la API del móvil, nuestra información de vecinos debe ser transmitida por LoRa. (No disponible en un canal con clave y nombre por defecto). + Intervalo de actualización + Modo IPv4 + Habilitar paquetes de difusión vía UDP en la red local. + Ethernet del Nodo Activado + Habilitar Ethernet desactivará la conexión bluetooth a la aplicación. Las conexiones TCP no están disponibles en dispositivos Apple. + DNS + Puerta de enlace + IP + Subred + Servidor NTP + Ninguna + Transmisión UDP + Servidor rsyslog + Habilitar WiFi desactivará la conexión bluetooth a la aplicación. + Contraseña + SSID (Nombre la Red) + Umbral mínimo de RSSI de BLE (por defecto es -80) + Activar el Contador de Paquetes + Intervalo de actualización + Distancia mínima + La distancia mínima de cambio en metros que se tendrá en cuenta para una transmisión inteligente de posición. + Intervalo mínimo + Posición fija + GPIO de habilitación GPS + Modo GPS (dispositivo físico) + Intervalo de actualización + Habilitado + Intervalo de transmisión + El intervalo máximo que puede transcurrir sin que un nodo transmita una posición. + Ubicación inteligente + Marcas de posición + Campos opcionales a incluir al ensamblar mensajes de posición. Cuantos más campos se incluyan, mayor será el tamaño del mensaje, lo que provocará un mayor tiempo de transmisión y un mayor riesgo de pérdida de paquetes. + Altitud + Altitud es Nivel Medio del Mar + Separació de Altitud Geoidal + Rumbo del vehículo + Número de satélites + Número de secuencia + Velocidad de vehículo + Marca de tiempo + GPIO de recepción GPS + GPIO de transmisión GPS + Sobreescribir relación del multiplicador ADC + Activar el modo ahorro de energía + La opción dormirá todo lo posible; los roles de rastreador y sensores y también incluirá la radio LoRa. No uses esta configuración si quieres utilizar tu dispositivo con las aplicaciones del teléfono o si estás usando un dispositivo sin botón de usuario. + Apagar al perder energía + Esperar Bluetooth durante + Test de alcance activado + Guardar el .CSV en el almacenamiento (Esp32) + Contraseña de administrador + Clave pública autorizada para enviar mensajes de administración a este nodo. + API de registro de depuración habilitada + Modo "ya terminado de configurar" + Dispositivo gestionado por administrador de la malla, el usuario no puede acceder a las configuraciones del dispositivo. + Clave privada + Utilizado para crear una clave compartida con un dispositivo remoto + Clave Pública + Consola serial + Tasa de baudios por segundo + Tasa de baudios por segundo + Eco activado + Comunicación serial activada + Modo serial + Por defecto + Por defecto + Tiempo agotado + Pulso de vida + Historial máximo devuelto + Servidor + Número de registros + Rol + Azul + Verde + Rojo + Módulo para la medición de la calidad del aire activado + Intervalo actualización de métricas calidad del aire + Enviar telemetría del dispositivo + Activar/Desactivar el módulo de telemetría del dispositivo para enviar métricas a la malla. Estos son valores nominales. Las mallas congestionadas escalarán automáticamente a intervalos más largos basados en el número de nodos en línea. Mallas con menos de nodos escalarán a intervalos más rápidos. + Intervalo actualización de métricas del dispositivo + Grados Fahrenheit para la temperatura ambiente + Módulo para las medidas del entorno activado + Mostrar las medidas del entorno en la + Intervalo actualización de métricas del entorno + Módulo de medidas eléctricas activado + Medidas eléctricas en pantalla + Intervalo de actualización de métricas de energía + diff --git a/core/resources/src/commonMain/composeResources/values-es/strings.xml b/core/resources/src/commonMain/composeResources/values-es/strings.xml index ee51e02ec4..05931466c0 100644 --- a/core/resources/src/commonMain/composeResources/values-es/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-es/strings.xml @@ -17,6 +17,8 @@ --> + Humedad + Temperatura: Acerca de Aceptar Agradecimientos @@ -24,7 +26,6 @@ Deseas eliminar el mensaje Acciones Sobreescribir multiplicador ADC - Sobreescribir relación del multiplicador ADC Añadir Añadir una nota privada… @@ -32,7 +33,6 @@ Añadir capa Añadir Dirección del Servidor MQTT - Contraseña de administrador Claves administración Administración Avanzado @@ -40,22 +40,12 @@ Avanzado Icono de la calidad del aire - Módulo para la medición de la calidad del aire activado - Intervalo actualización de métricas calidad del aire Porcentaje de tiempo de transmisión utilizado en la última hora. - - Notificación con el zumbador al recibir una campana - Notificación LED al recibir una campana ¡Carácter Campana de Alerta! - Notificación con vibración al recibir una campana - Notificación con el buzzer al recibir un mensaje - Notificación LED al recibir un mensaje - Notificación con vibración al recibir un mensaje Todos Permitir el acceso sin un pin definido Altitud Altitud - Siempre apuntar al norte Luz Ambiental Configuración de la luz ambiental Se recopilan analíticas de uso para ayudarnos a mejorar la aplicación Android (¡gracias!), recibiremos información anónima sobre el comportamiento del usuario. Esto incluye reportes de fallos, pantallas utilizadas en la aplicación, etc. @@ -74,23 +64,14 @@ Configuración de sonido Pines disponibles Mal - - Ancho de Banda Batería Dirección I2C del INA_2xx para la batería - Umbral mínimo de RSSI de BLE (por defecto es -80) - Azul Bluetooth Configuración Bluetooth - Bluetooth activado Configuración Bluetooth - Encabezado en negrita Ajustes - Intervalo de transmisión - Botón GPIO - Zumbador GPIO Calculando… Permisos de acceso a la cámara Cancelar @@ -100,7 +81,6 @@ Mensaje predefinidos activados No se puede cambiar de canal porque la radio aún no está conectada. Por favor inténtelo de nuevo. Apagado no compatible con este dispositivo - Intervalo de carrusel Utilización del canal actual, incluyendo TX, RX bien formado y RX mal formado (ruido similar). Canal 1 @@ -113,7 +93,6 @@ Canal 8 Características del canal La URL de este canal no es válida y no puede utilizarse - Canal Nombre del canal Canales @@ -130,45 +109,20 @@ Limpiar selección Cerrar Cerrar selección - CODEC 2 activado - Frecuencia de Muestreo CODEC2 - Tasa de codificación Distancia: %1$s Este dispositivo no tiene un sensor de brújula. El rumbo no está disponible. La brújula apunta al norte - Orientación de la brújula Brújula Las claves se han visto comprometidas, selecciona OK para generar de nuevo. - Trate un doble toque en acelerómetros soportados como una pulsación de botón de usuario. Controla el LED parpadeante del dispositivo. Para la mayoría de los dispositivos esto controlará uno de los hasta 4 LEDs, el cargador y el GPS tienen LEDs no controlables. - Si, además de enviarlo a MQTT y a la API del móvil, nuestra información de vecinos debe ser transmitida por LoRa. (No disponible en un canal con clave y nombre por defecto). Envía la posición al canal primario cuando el botón de usuario se presiona tres veces. Zona horaria para fechas en la pantalla del dispositivo y registro. Utilizar zona horaria del teléfono - Cambia automáticamente a la siguiente página en la pantalla como un carrusel, basado en el intervalo especificado. - La dirección de la brújula en la pantalla, fuera del círculo, siempre apuntará hacia el norte. - Anula el diseño de pantalla predeterminado. - Voltear la pantalla verticalmente. - Encabezado del texto de la pantalla en negrita. - Anular detección automática de pantalla OLED. - Cuánto tiempo permanece encendido la pantalla después de pulsar el botón de usuario o recibir mensajes. - Unidades mostradas en la pantalla del dispositivo. - Requiere que haya un acelerómetro en su dispositivo. - La frecuencia de funcionamiento de su nodo se calcula en base a la región, el preajuste del módem y este campo. Cuando es 0, la ranura se calcula automáticamente basándose en el nombre del canal principal y cambiará de la rama pública predeterminada. Cambie a la ranura pública por defecto si se configuran los canales privados primarios y públicos secundarios. - Establece el número máximo de saltos, por defecto 3. Aumentar saltos también incrementa la congestión y debe ser utilizado con cuidado. 0 saltos de difusión no obtendrán ACKs. Configuraciones de modem disponibles, por defecto es Long Fast. La región donde utilizará su radio. - Habilitar Ethernet desactivará la conexión bluetooth a la aplicación. Las conexiones TCP no están disponibles en dispositivos Apple. Habilitar paquetes de difusión vía UDP en la red local. - El intervalo máximo que puede transcurrir sin que un nodo transmita una posición. - La distancia mínima de cambio en metros que se tendrá en cuenta para una transmisión inteligente de posición. - La máxima velocidad a la que se enviarán las actualizaciones de posición si se ha cumplido la distancia mínima. - Campos opcionales a incluir al ensamblar mensajes de posición. Cuantos más campos se incluyan, mayor será el tamaño del mensaje, lo que provocará un mayor tiempo de transmisión y un mayor riesgo de pérdida de paquetes. - La opción dormirá todo lo posible; los roles de rastreador y sensores y también incluirá la radio LoRa. No uses esta configuración si quieres utilizar tu dispositivo con las aplicaciones del teléfono o si estás usando un dispositivo sin botón de usuario. - Clave pública autorizada para enviar mensajes de administración a este nodo. - Dispositivo gestionado por administrador de la malla, el usuario no puede acceder a las configuraciones del dispositivo. Utilizado para crear una clave compartida con un dispositivo remoto. Configuración Configurar alertas críticas @@ -203,7 +157,6 @@ Borrar todos los filtros Filtro incluido Filtros - API de registro de depuración habilitada Exportar registros Panel de depuración Borrar búsqueda @@ -228,18 +181,13 @@ Detalles Sensor de Presencia Configuración del sensor detector - Sensor detector activado - Tipo de detección para activar Dispositivo Configuración del dispositivo Límite de caché DB del dispositivo Dispositivo GPS Métricas de Dispositivo - Intervalo actualización de métricas del dispositivo Dispositivo en reposo - Enviar telemetría del dispositivo - Activar/Desactivar el módulo de telemetría del dispositivo para enviar métricas a la malla. Estos son valores nominales. Las mallas congestionadas escalarán automáticamente a intervalos más largos basados en el número de nodos en línea. Mallas con menos de nodos escalarán a intervalos más rápidos. Punto de rocío Mensaje Directo Claves para mensaje directo @@ -253,18 +201,13 @@ Mensajes Seleccionados Disco libre %1$d - Pantalla - Modo de la pantalla - Si activado, el dispositivo mostrará la hora en la pantalla en el formato de 12h - Unidades en pantalla Distancia Filtros de distancia Filtra la lista de nodos y el mapa de mallas basándose en la proximidad a tu teléfono. Mediciones de Distancia Muestra la distancia entre tu teléfono y otros nodos Meshtastic posicionados. - DNS Borrar búsqueda Conexiones @@ -272,30 +215,20 @@ Nodos Primeros Pasos Hecho - Doble pulsación como botón Descarga + Dinámico - Eco activado Editar 8 Horas - Activar el modo ahorro de energía Habilitado - - Permitir Encripción Clave pública no coincide Cifrado de Clave Pública Métricas de Entorno - Ambiente - Módulo para las medidas del entorno activado - Mostrar las medidas del entorno en la - Intervalo actualización de métricas del entorno - Grados Fahrenheit para la temperatura ambiente Error Límite de Ciclo de Trabajo alcanzado. No se pueden enviar mensajes en este momento, por favor inténtalo de nuevo más tarde. Opciones Ethernet - Ethernet del Nodo Activado IP Ethernet: Intercambiar Posición Caduca @@ -304,7 +237,6 @@ Exportar todos los paquetes Notificaciones Externas Configuración de las notificaciones externas - Notificaciones externas activadas Restablecer los valores de fábrica Aceptable (No se ha podido obtener el nombre) Meshtastic %1$s @@ -336,62 +268,35 @@ Actualización de firmware Modelo de hardware desconocido: %1$d Versión del firmware: %1$s - Pin fijo Posición Fijada - Girar la pantalla 180º Para más información, consulte nuestra política de privacidad. Memoria disponible Frecuencia - Slot de frecuencia - Mote Resistencia del Gas - Puerta enlace - Hacer una acción al girar en sentido contrario a las agujas del reloj - Hacer una acción al girar en sentido de las agujas del reloj - Hacer una acción al presionar Empezar Bien - GPIO Pin GPIO - Pin A GPIO del Encoder - Pin B GPIO del Encoder - Pin de la Pulsación del Encoder - Pin GPIO para monitorizar - GPIO de habilitación GPS - Modo GPS (dispositivo físico) - GPIO de recepción GPS - GPIO de transmisión GPS - Verde Hardware Modelo del dispositivo Rumbo - Pulso de vida Ocultar capa Ocultar contraseña - Historial máximo devuelto - Número de saltos Saltos de distancia Anfitrión Métricas del anfitrión Estoy de acuerdo. He leído y entiendo lo anterior. Doy mi consentimiento voluntario para la transmisión no cifrada de los datos de mi nodo a través de MQTT. Sé lo que estoy haciendo - Reloj del I2S - Entrada de datos I2S - Salida de datos I2S - Selecionar palabras I2S IAQ (Calidad de Aire interior) escala relativa del valor IAQ como mediciones del sensor Bosch BME680. Rango de Valores 0 - 500. Significado de los iconos - Ignorar ¿Añadir '%1$s' para ignorar la lista? Tu radio se reiniciará después de hacer este cambio. Ignorar entrante - Ignorar Paquetes MQTT ¿Eliminar '%1$s' para ignorar la lista? Tu radio se reiniciará después de hacer este cambio. Importar una configuración @@ -409,7 +314,6 @@ Rango de Valores 0 - 500. IP Dirección IP: Puerto: - Modo IPv4 Salida JSON activada Filtrar por tiempo de la última escucha: %1$s @@ -419,8 +323,6 @@ Rango de Valores 0 - 500. Latitud Saber más - Latido LED - Estado de led Canal de administración antiguo Activando esta opción se desactiva la encriptación y deja de ser compatible con la red de Meshtastic normal. @@ -453,11 +355,9 @@ Rango de Valores 0 - 500. Batería baja: %1$s Lux Administrar capas de mapa - Modo "ya terminado de configurar" Requerir solicitud manual de posición Mapa Mesh - Capacidad del caché: %1$d MB\nUso del caché: %2$d MB Gestor de Caché Tamaño actual de Caché %1$d Fichas @@ -472,12 +372,9 @@ Rango de Valores 0 - 500. Administrador sin conexión Error en la purga del caché SQL, consulte logcat para obtener más detalles Caché SQL purgado para %1$s - Reportar Posición Consentimiento para compartir información del nodo sin encriptar por MQTT Al habilitar esta función, reconoces y das tu consentimiento expreso para la transmisión de la ubicación geográfica en tiempo real de tu dispositivo a través del protocolo MQTT sin ser esta encriptada. Estos datos de ubicación pueden ser utilizados para fines como aparecer en un mapa en directo, rastrear el dispositivo y funciones de telemetría relacionadas. - Tiempo entre Reportes de Posición (en Segundos) - Tu nodo enviará periódicamente un paquete sin encriptar de posición en el mapa al servidor MQTT configurado. Esto incluye id, nombre largo y corto, ubicación aproximada, modelo de hardware, rol, versión del firmware, región LoRa, pre-ajuste del módem y nombre del canal principal. Seleccionar región de descarga Comenzar Descarga rumbo: %1$d° distancia: %2$s @@ -516,7 +413,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m En cola para enviar Desconocido Mensajes - Tiempo mínimo de transmisión (segundos) Predefinidos Configuración de módulo Módulos ya desbloqueados @@ -524,7 +420,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m MQTT Configuración MQTT - Activar el MQTT Conectado Desconectado ¡Debe establecer una región! @@ -538,14 +433,12 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Silenciado por %1$d días, %2$s horas Silenciado por %1$s horas No silenciado - Nombre Nombre no puede estar vacío. Ir atrás Navegar hacia Información de Vecinos Configuración de Información de Vecinos - Información de Vecinos Conexión Red Nueva URL de canal recibida Nuevos mensajes abajo @@ -580,7 +473,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m vía Favorita vía MQTT Reinicio de NodeDB - Intervalo de transmisión de información del nodo Nodos Nodos en esta ubicación @@ -591,18 +483,15 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Ninguna No está conectado Notas + Meshtastic utiliza las notificaciones para mantenerte actualizado sobre nuevos mensajes y otros eventos importantes. Puedes actualizar tus permisos de notificación en cualquier momento desde la configuración. Notificaciones para nodos recién descubiertos. Notificaciones al recibir una alerta/campana Notificación cuando se reciba un mensaje Ahora - Servidor NTP - Número de registros - Permitir MQTT Vale - Tipo de OLED 24 Horas 1 hora @@ -613,23 +502,12 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Abrir ajustes Opciones Orientación norte - - Salida buzzer (pin GPIO) - Duración en las salidas (milisegundos) - LED de salida activo en alto - Salida LED (pin GPIO) - Salida vibratoria (pin GPIO) Sobreescribir el puerto serie de la consola - Sobreescribir el Tiempo de Trabajo - Sobreescribir frecuencia - Ventilador del Amplificador apagado - Modo de emparejamiento Contraseña Contador de Paquetes Configuración del Contador de Paquetes - Activar el Contador de Paquetes Transmisión periódica de posición Meshtastic necesita activar los permisos "Dispositivos cercanos" para encontrar y conectarse a dispositivos mediante Bluetooth. Puede desactivar cuando no esté en uso. @@ -641,20 +519,16 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m %1$d hora %1$d horas + Posición Definir desde la ubicación actual del teléfono Posición activada - Marcas de posición Posición Paquetes de posición - Consumo Configuración de elecenergía Métricas de Energía - Módulo de medidas eléctricas activado - Medidas eléctricas en pantalla - Intervalo de actualización de métricas de energía Ubicación precisa Idioma Predeterminado del sistema @@ -664,12 +538,9 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Primario Transmisión periódica de la posición y la telemetría - Clave privada Proporcionar la ubicación del teléfono a la malla El nombre ya existe. - Compartir Internet a la Radio PSK (Contraseña) - Pin PTT Clave Pública Contraseña pública cambiada Código QR @@ -685,22 +556,11 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Configuración de radio Test de Alcance Configuración del test de alcance - Test de alcance activado Reaccionar Reiniciar - - Modo de retransmisión - Si está en nuestro canal privado o desde otra red con los mismos parámetros lora, retransmite cualquier mensaje observado. - Igual al comportamiento que TODOS pero omite la decodificación de paquetes y simplemente los retransmite. Sólo disponible en el rol repetidor. Establecer esto en cualquier otro rol dará como resultado TODOS los comportamientos. - Ignora paquetes de puertos no estándar, tales como los TAK, Test de Rango (Rangetest), Contador de paquetes (Pax), etc. Solo retransmite paquetes que vengan de puertos estándar como: Información de Nodo (NodeInfo), Mensajes de texto, Posición, telemetría y Routing. - Ignora los mensajes recibidos de redes externas como LOCAL ONLY, pero ignora también mensajes de nodos que no están ya en la lista de nodos conocidos. - Ignora mensajes observados desde mallas foráneas que están abiertas o que no pueden descifrar. Solo retransmite mensajes en los nodos locales principales / canales secundarios. - Solo permitido para los roles SENSOR, TRACKER y TAK_TRACKER, esto inhibirá todas las retransmisiones, no a diferencia del rol de CLIENT_MUTE. Dispositivos de red recientes - Rojo ¿Estás seguro de querer regenerar tu clave privada?\n\nLos nodos que hayan intercambiado previamente las claves con este nodo tendrán que quitar ese nodo y volver a intercambiar las claves para poder reanudar la comunicación segura. Regenerar clave privada - Región Remoto Administración remota @@ -736,29 +596,16 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Rol del dispositivo Cliente Base cliente - Aplicación conectada o dispositivo de mensajería autónomo. Cliente oculto - Dispositivo que solo emite según sea necesario por sigilo o para ahorrar energía. Cliente silenciado - El dispositivo no reenvía mensajes de otros dispositivos. Perdido y encontrado Repetidor - Un nodo que es parte de infraestructura para extender el rango de esta misma, reemitiendo mensajes de nodos con poco alcance. No aparecerá en la lista de nodos visibles. Router Cliente de router - Combinación de ROUTER y CLIENTE. No para dispositivos móviles. - Nodo de infraestructura para ampliar la cobertura de la red mediante la retransmisión de mensajes. Visible en la lista de nodos. - Nodo de infraestructura que permite la retransmisión de paquetes una vez posterior a los demás modos, asegurando cobertura adicional a los grupos locales. Es visible en la lista de nodos. Sensor - Transmite paquetes de telemetría como prioridad. TAK - Optimizado para el sistema de comunicación ATAK, reduciendo las transmisiones rutinarias. Rastreador TAK - Permite la transmisión automática TAK PLI y reduce las transmisiones rutinarias. Rastreador - Transmisión de paquetes de posición GPS como prioridad. - Tema raíz - Encoder Número 1 Activado Recibido un reconocimiento negativo Sin ruta @@ -766,16 +613,12 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Tiempo agotado RSSI Indicador de Fuerza de Señal Recibida (RSSI en inglés), una medida utilizada para determinar el nivel de potencia que está siendo recibido por la antena. Un valor de RSSI más alto generalmente indica una conexión más fuerte y estable. - Servidor rsyslog Satélites - Guardar Guardar - Guardar el .CSV en el almacenamiento (Esp32) Exportar paquetes de rangetest Escanear - Pantalla activa durante Desplazarse hacia abajo Secundario Deshabilitar la posición en el canal principal permite las emisiones de posición periódica en el primer canal secundario con la posición habilitada, de lo contrario se requiere una solicitud de posición manual. @@ -797,17 +640,8 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Seleccionados Tipo de mapa seleccionado Enviar - Mandar campana 🔔 - Mandar la campana con el mensaje de alerta - Periodo entre los mensajes del transmisor (segundos) - Conexión Serial - Tasa de baudios por segundo Configuración serial - Consola serial - Comunicación serial activada - Modo serial - Servidor Introduzca su región ajustes @@ -826,7 +660,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Mostrar marcas de posición Apagar Nodo: %1$s - Apagar al perder energía ⚠️ Esto APAGARÁ el nodo. Se necesitará interacción física para volver a encenderlo. Señal Calidad de señal @@ -834,28 +667,19 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Pantalla Saltar Ranura - Ubicación inteligente SNR SNR: Ratio de señal a ruido, una medida utilizada en las comunicaciones para cuantificar el nivel de una señal deseada respecto al nivel del ruido de fondo. En Meshtastic y otros sistemas inalámbricos, un mayor SNR indica una señal más clara que puede mejorar la fiabilidad y la calidad de la transmisión de datos. Velocidad - Factor de dispersión - SSID (Nombre la Red) - Periodo entre transmisión de estado (segundos) Manténgase conectado en cualquier lugar - Subred Duración del sueño súper profundo Soportado nativamente Apoyado por la comunidad Meshtastic Eliminar Silenciar Desilenciar - Aumentar ganancia de RX Ajustes del sistema Servidor - Azul - Verde - Rojo Telemetría Configuración de la telemetría Tema @@ -863,10 +687,8 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Claro Predeterminado del sistema Tiempo - Zona horaria Tiempo agotado Fecha - TLS activado Cambiar mi posición Trazar ruta @@ -884,7 +706,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Rastrear y comparte ubicaciones - Transmitir en LoRa BLE LoRa @@ -893,10 +714,10 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m 24H 48 Horas 2Semanas - Transmisión Activa - Potencia de transmisión Tipo Escribe un mensaje + dBm + m Predeterminado del sistema Métrico @@ -910,8 +731,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Desilenciar No reconocido Sin establecer - 0 - Arriba/Abajo/Seleccionar Activado - Intervalo de refresco (segundos) Los mensajes de la malla se enviarán a la internet pública a través de la puerta de enlace configurada de cualquier nodo. Tiempo encendido @@ -919,24 +738,15 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m URL no puede estar vacío. La URL debe contener marcadores de posición. Plantilla de URL - - Utilizar el formato de 12h para el reloj - Utilizar el Buzzer como uno I2S - Utilizar el modo de entrada PULL_UP - Usar predefinido - Utilizar buzzer PWM Usuario Configuración del Usuario Identificación del usuario Cadena del usuario - Usuario Lux ultravioletas vía MQTT Ver en el mapa Tensión - Esperar Bluetooth durante - Despertar al tocar o al mover Advertencia ¿Eliminar punto de referencia? Editar punto de referencia diff --git a/core/resources/src/commonMain/composeResources/values-et/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-et/schema_strings.xml new file mode 100644 index 0000000000..20aeb4178e --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-et/schema_strings.xml @@ -0,0 +1,283 @@ + + + + + Sinine + Pinge + Roheline + LED olek + Punane + Vaikimisi + CODEC2 test sagedus + CODEC 2 lubatud + I2S info välja + I2S kell + I2S info sisse + I2S sõna valik + PTT klemm + Sinihammas lubatud + Sidumine + Sisendsündmus CCW pordis + Sisendsündmus CW pordis + Sisendsündmus Vajuta pordis + GPIO klemmi pöördvalik A pordis + GPIO klemmi pöördvalik B pordis + GPIO klemmi pöördvalik Vajuta pordis + Tagasi + Tühista + Puudub + Vali + Pöördvalik #1 lubatud + Saada Kõll + Üles/Alla/Vali sisend lubatud + Identifitseerimistüüp + Tuvastusandur lubatud + GPIO klemmi jälgimine + Kasutajasõbralik nimi + Saada kõll koos hoiatussõnumiga + Kõrge + Kasuta INPUT_PULLUP režiimi + Nupu GPIO + Summeri GPIO + Topeltpuudutus nupuna + Topelt puudutust toetatud kiirendusmõõturitel käsitletakse kasutaja nupuvajutusena. + Südamelöögi LED + Sõlme teabe edastamise intervall + Kordusülekannete režiim + Kõik + Vahelejäetud kõik dekodeerimine + Sama käitumine nagu KÕIK puhul, aga pakete ei dekodeerita vaid need lihtsalt edastatakse. Saadaval ainult Repiiteri rollis. Teise rolli puhul toob kaasa KÕIK käitumise. + Ainult põhipordi numbriga + Ainult teadaolevad + Ainult kohalik + Ignoreerib jälgitavaid sõnumeid välis kärgvõrkudest avatud või dekrüpteerida mittevõimalikke sõnumeid. Saadab sõnumeid ainult kohalikel primaarsetel/sekundaarsetel kanalitel. + Puudub + Lubatud ainult rollidele SENSOR, TRACKER ja TAK_TRACKER, see blokeerib kõik kordus edastused, vastupidiselt CLIENT_MUTE rollile. + Seadme roll + Klient + Klient-baas + Peidetud klient + Seade, mis edastab ülekandeid ainult siis, kui see on vajalik varjamiseks või energia säästmiseks. + Vaikne klient + Seade, mis ei edasta pakette teistelt seadmetelt. + Kaotud ja leitud + Edastab asukohta regulaarselt vaikekanalile sõnumina, et aidata seadme leidmisel. + Repiiter + Ruuter + Ruuteri klient + Hiline ruuter + Andur + Esmalt edastatakse telemeetria pakette. + TAK + Optimeeritud ATAK süsteemi side jaoks, vähendab rutiinseid saateid. + Jälgitav TAK + Võimaldab automaatseid TAK PLI saateid ja vähendab rutiinseid saateid. + Jälgitav + Esmajärjekorras edastatakse GPS asukoha pakette. + Ajavöönd + Karusselli intervall + Läheb automaatselt ekraani järgmisele lehele nagu karussell, vastavalt määratud intervallile. + Suund alati põhi + Ekraanil ringist väljaspool olev kompassi suund osutab alati põhja suunas. + Kompassi suund + Ekraani režiim + Tühista vaikimisi ekraanipaigutus. + Imperial + Meetriline + Keera ekraani + Pööra ekraani vertikaalselt. + Paks pealkiri + Paksenda ekraani pealkirjatekst. + OLED tüüp + Tühista automaatse OLED-ekraani tuvastamine. + Ekraan sisse lülitatud + Kui kaua ekraan pärast nupu vajutamist või sõnumite vastuvõtmist sisse lülitatuks jääb. + Ekraani ühikud + Seadme ekraanil kuvatavad ühikud. + Kasuta 12 tunni formaati + Kui see on lubatud, kuvab seade ekraanil aega 12 tunni formaadis. + Ärata puudutusega või liigutusega + Vajalik, et teie seadmes oleks kiirendusmõõtur. + Väljund LED aktiivne + Hoiatuskella LED + Hoiatuskella summer + Hoiatuskella värin + Hoiatussõnumi LED + Hoiatussõnumi summer + Hoiatussõnumi värin + Luba Välised teated + Väljund LED (GPIO) + Väljund summer (GPIO) + Väljund värin (GPIO) + Kasuta I2S summerina + Kasuta PWM summerit + Ribalaius + Sageduspesa + Teie sõlme töösagedus arvutatakse piirkonna, modemi eelseadistuse ja selle välja põhjal. Kui see on 0, arvutatakse pesa automaatselt primaarse kanali nime põhjal ja see muutub vaikimisi avalikust pesast. Muutke see tagasi avalikuks vaikepesaks, kui privaatsed primaarsed ja avalikud sekundaarsed kanalid on konfigureeritud. + Kodeerimiskiirus + Ok MQTTi + Hüpete arv + Määrab hüpete maksimaalse arvu, vaikimisi on see 3. Hüpete suurendamine suurendab ka ülekoormust ja seda tuleks kasutada ettevaatlikult. 0 hüppega leviedastussõnumid ei saa ACK-sid. + Keela MQTT + Eelseadistused + Kerge - Kiire + Kerge - Aeglane + Pikk ulatus - kiire + Pikk ulatus - mõõdukas + Pikk ulatus - aeglane + Pikk ulatus - Turbo + Keskmine ulatus - kiire + Keskmine ulatus - aeglane + Medium Range - Turbo + Kitsas - Kiire + Kitsas - Aeglane + Lühike ulatus - kiire + Lühike ulatus - aeglane + Lühike ulatus - turbo + Pisike - kiire + Pisike - aeglane + Väga pikk ulatus- aeglane + Töötsükli tühistamine + Tühista sagedus + PA ventilaator keelatud + Regioon + Raadio kasutama hakkamise piirkond. + Levitustegur + RX võimendatud võimendus + Edastus lubatud + Saatevõimsus + Kasuta eelseadistust + Luure + Meedik + Snaiper + Meeskonna ülem + Meeskonnaliige + Sõnum + Aadress + MQTT lubatud + Krüpteerimine lubatud + Kaardiaruannete koostamine + Parool + Kliendi proksi lubatud + Juurteema + Kasutajatunnus + Naabruskonna teave lubatud + Saada LoRa kaudu + Lisaks MQTT-le ja PhoneAPI-le saatmisele peaks meie NeighborInfo edastama ka LoRa kaudu. Ei tööta vaikimisi võtme ja - nimega kanalil. + GPS-i küsimise intervall + IPv4 režiim + Luba kohalikus võrgus pakettide edastamine UDP kaudu. + Ethernet lubatud + Etherneti lubamine keelab sinihamba ühenduse rakendusega. TCP-sõlmede ühendused pole Apple'i seadmetes saadaval. + DNS + Lüüs + IP + Alamvõrk + NTP server + Puudub + UDP edastus + rsyslog server + Wi-Fi lubatud + WiFi lubamine keelab rakenduses Bluetooth-ühenduse. + Parool + SSID + BLE RSSI lävi (vaikeväärtus -80) + Paxcounter lubatud + GPS-i küsimise intervall + Nutikas kaugus + Nutika asukoha edastamisel arvestatav minimaalne kauguse muutus meetrites. + Nutikas intervall + Määratud asukoht + GPS EN GPIO + GPS-režiim (riistvara) + GPS-i küsimise intervall + Kui tihti peaksime proovima GPS asukohta määrata (<10sekundit hoiab GPSi sisselülitatuna). + Keelatud + Lubatud + Levitamise inteintervall + Maksimaalne intervall, mille jooksul sõlm ei levita oma asukohta. + Nutikas asukoht + Asukoha lipp + Valikulised väljad lisatakse asukohasõnumitele, mida rohkem välju, seda pikem sõnum – see pikendab eetriaega ja suurendab pakettide kadumise ohtu. + Kõrgus + Ajatempel + GPS vastuvõtu GPIO + GPS saatmise GPIO + Asenda ADC kordistaja suhe + Luba energiasäästurežiim + Unereziimis nii palju kui võimalik, jälgitava ja anduri rolli puhul hõlmab see ka Lora raadiot. Ärge kasutage seda sätet, kui soovite oma seadet kasutada telefonirakendustega või kui kasutate seadet ilma kasutajanuputa. + Väljalülitamine voolukatkestuse korral + Oota Bluetoothi ​​kestust + Ulatustest lubatud + Salvesta .CSV faili (ainult ESP32) + Administraatori võti + Avalik võti, millel on õigus sellele sõlmele administraatori sõnumeid saata. + Silumislogi API lubatud + Väljunda reaalajas arendajalogi jadapordi kaudu, vaata ja salvesta asukoha redigeerimisega seadmelogisid sinihamba ​​kaudu. + Hallatud režiim + Seadet haldab kärgvõrgu administraator, kasutajal pole juurdepääsu seadme sätetele. + Salajane võti + Avalik võti + Jadapordi konsool + Jadapordi konsool voog API kaudu. + Jadapordi kiirus + Jadapordi kiirus + Kaja lubatud + Jadaport lubatud + Jadapordi režiim + RX + Vaikimisi + Vaikimisi + Aegunud + TX + Salvesta & edastamine lubatud + Südamelöögid + Ajalookirjete maksimaalne arv + Ajalookirjete aken + Server + Kirjete arv + Roll + Tiim + Sinine + Pruun + Tsüaan + Tume sinine + Tume roheline + Roheline + Fukspunane + Kastanpruun + Oranž + Lilla + Punane + Sinakasroheline + Valge + Kollane + Õhukvaliteedi moodul on lubatud + Õhukvaliteedi näidikute värskendamise intervall + Saada seadme telemeetria + Luba/keela seadme telemeetriamoodulil andmete saatmine kärgvõrku. Need on nimiväärtused. Ülekoormatud kärgvõrgu puhul skaleeritakse need automaatselt pikemale intervallile olenevalt võrgus olevate sõlmede arvule. + Seadme mõõdikute värskendamise intervall + Keskkonnamõõdikud kasutavad Fahrenheiti + Keskkonnamõõdikute lubamine + Keskkonnamõõdikute ekraanil kuvamine lubatud + Keskkonnamõõdikute värskendamise intervall + Toitemõõdiku moodul on lubatud + Toitemõõdiku ekraanil kuvamine lubatud + Toitemõõdikute värskendamise intervall + Tundmatu pakettide lävi + diff --git a/core/resources/src/commonMain/composeResources/values-et/strings.xml b/core/resources/src/commonMain/composeResources/values-et/strings.xml index 095cd6aac0..e4f67ec181 100644 --- a/core/resources/src/commonMain/composeResources/values-et/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-et/strings.xml @@ -17,18 +17,20 @@ --> + Niiskus %1$s: %2$s Sõnum saatjalt %1$s: %2$s aku %1$d% + Kanal %1$d %1$s kaugusel lemmik - %1$d hüppe kaugusel viimati kuuldud %1$s ühenduseta ühenduses roll %1$s signaal %1$s Hüpet %1$d: %2$d seadmeni + Temperatuur Teave Nõustu Tänusõnad @@ -46,7 +48,6 @@ Tõlgi sõnum Toimingud ADC kordaja tühistamine - Asenda ADC kordistaja suhe ADC pinge Lisa @@ -60,7 +61,6 @@ Lisa sead käsitsi… Lisa kaardikiht Aadress - Administraatori võti Admin võtmed Haldus Täpsem @@ -70,24 +70,14 @@ Õhu kvaliteet Õhukvaliteedi ikoon Õhu kvaliteedi näidiku logi - Õhukvaliteedi moodul on lubatud - Õhukvaliteedi näidikute värskendamise intervall Viimase tunni jooksul kasutatud eetriaja protsent. Saate kasutus - - Hoiatuskella summer - Hoiatuskella LED Häirekella sümbol! - Hoiatuskella värin - Hoiatussõnumi summer - Hoiatussõnumi LED - Hoiatussõnumi värin Kõik Luba sisend allikas Luba määratlemata klemmi juurdepääs Kõrgus Kõrgus - Suund alati põhi Ambient valgus Ambient valguse sätted Analüüsiandmeid kogutakse Androidi rakenduse täiustamiseks (tänan). Me saame anonüümset teavet kasutajate käitumise kohta. See hõlmab krahhiaruandeid, rakenduse ekraanipilte jms. @@ -135,8 +125,6 @@ Salvestab avalikud ja privaatvõtmed selle seadme turvalisse ja krüpteeritud salvestusruumi. Varunda & Taasta Halb - - Ribalaius Vaikimisi (%1$s kHz) %1$s kHz Toetamata (%1$s) @@ -145,14 +133,11 @@ Aku Aku INA_2XX I2C aadress Sinihamba seade - BLE RSSI lävi (vaikeväärtus -80) - Sinine Sinihammas Saadaval olevad sinhamba seadmed Sinihamba sätted Sinihammas on välja lülitatud. Lähedalasuvate seadmete otsimiseks lülita see sisse. - Sinihammas lubatud Sätted Halda juhtmevabalt seadme sätteid ja kanaleid. Avastamine @@ -165,14 +150,10 @@ Sinihamba skannimise limiit on saavutatud. Proovi uuesti %1$d sekundi pärast. Sinihamba skannimise limiit on saavutatud. Proovi uuesti %1$d sekundi pärast. - Paks pealkiri Sidumine ebaõnnestus. Anna lähedal asuvatele seadmetele luba ja proovi uuesti. Sidumine ebaõnnestus. Proovi siduda uuesti. Sätted - Levitamise inteintervall Hõivatuse tase - Nupu GPIO - Summeri GPIO Selle vähendamine kustutab jäädavalt %1$d seadme salvestatud ajaloo. Selle vähendamine kustutab jäädavalt %1$d seadmete salvestatud ajaloo. @@ -190,7 +171,6 @@ Salvestatud sõnumid lubatud Kanalit ei saanud vahetada, kuna raadio pole veel ühendatud. Proovi uuesti. Seade ei toeta väljalülitamist - Karusselli intervall Praeguse kanali kasutamine, sealhulgas korrektne TX, RX ja vigane RX (ehk müra). Kanal @@ -204,7 +184,6 @@ Kanal 8 Kanali funktsioonid Kanali URL on kehtetu ja seda ei saa kasutada - Kanal Kanali nimi Kanali URL Kanali kasutus @@ -249,9 +228,6 @@ CO₂ CO₂ Nsk CO₂ Temp - CODEC 2 lubatud - CODEC2 test sagedus - Kodeerimiskiirus Ahenda diagrammi Langenud Saada sõnumeid sõpradele ja kommuunile ilma võrguühenduse või mobiilivõrguta. @@ -264,48 +240,23 @@ Kauguse ja suuna kuvamiseks on vaja asukoha luba. Sellel seadmel pole kompassiandurit. Suund pole saadaval. Kompassi põhjasuund üleval - Kompassi suund Kompass Hinnanguline piirkond: \u00b1%1$s (\u00b1%2$s) Hinnanguline piirkond: täpsus teadmata Tuvastati ohustatud võtmed, valige uuesti loomiseks OK. - Topelt puudutust toetatud kiirendusmõõturitel käsitletakse kasutaja nupuvajutusena. Juhib seadme vilkuvaid LED. Enamiku seadmete puhul juhib see ühe kuni 4 LED, laadija ja GPS LED ei ole juhitavad. - Lisaks MQTT-le ja PhoneAPI-le saatmisele peaks meie NeighborInfo edastama ka LoRa kaudu. Ei tööta vaikimisi võtme ja - nimega kanalil. Saada asukoht põhikanalil, kui klõpsatakse kasutaja nuppu kolm korda. Seadme ekraanil ja logis kuvatavate kuupäevade ajavöönd. Kasuta telefoni ajavööndit - Läheb automaatselt ekraani järgmisele lehele nagu karussell, vastavalt määratud intervallile. - Ekraanil ringist väljaspool olev kompassi suund osutab alati põhja suunas. - Tühista vaikimisi ekraanipaigutus. - Pööra ekraani vertikaalselt. - Paksenda ekraani pealkirjatekst. - Tühista automaatse OLED-ekraani tuvastamine. - Kui kaua ekraan pärast nupu vajutamist või sõnumite vastuvõtmist sisse lülitatuks jääb. - Seadme ekraanil kuvatavad ühikud. - Vajalik, et teie seadmes oleks kiirendusmõõtur. - Teie sõlme töösagedus arvutatakse piirkonna, modemi eelseadistuse ja selle välja põhjal. Kui see on 0, arvutatakse pesa automaatselt primaarse kanali nime põhjal ja see muutub vaikimisi avalikust pesast. Muutke see tagasi avalikuks vaikepesaks, kui privaatsed primaarsed ja avalikud sekundaarsed kanalid on konfigureeritud. - Määrab hüpete maksimaalse arvu, vaikimisi on see 3. Hüpete suurendamine suurendab ka ülekoormust ja seda tuleks kasutada ettevaatlikult. 0 hüppega leviedastussõnumid ei saa ACK-sid. Selle piirkonna eelseadistused on ainult litsentseeritud (amatöörraadio) operaatoritele. Nende valimiseks luba kasutaja konfis litsentseeritud amatöörraadio (amatöör). Saadaval olevad modemi eelseadistused, vaikesäte on Long Fast. Raadio kasutama hakkamise piirkond. - Etherneti lubamine keelab sinihamba ühenduse rakendusega. TCP-sõlmede ühendused pole Apple'i seadmetes saadaval. Luba kohalikus võrgus pakettide edastamine UDP kaudu. - Maksimaalne intervall, mille jooksul sõlm ei levita oma asukohta. - Nutika asukoha edastamisel arvestatav minimaalne kauguse muutus meetrites. - Asukohavärskendused saadetakse kiiremini, kui minimaalne vahemaa on saavutatud. - Valikulised väljad lisatakse asukohasõnumitele, mida rohkem välju, seda pikem sõnum – see pikendab eetriaega ja suurendab pakettide kadumise ohtu. - Kui tihti peaksime proovima GPS asukohta määrata (<10sekundit hoiab GPSi sisselülitatuna). - Unereziimis nii palju kui võimalik, jälgitava ja anduri rolli puhul hõlmab see ka Lora raadiot. Ärge kasutage seda sätet, kui soovite oma seadet kasutada telefonirakendustega või kui kasutate seadet ilma kasutajanuputa. Sõlm taaskäivitub ja on lühikeseks ajaks kättesaamatu. - Avalik võti, millel on õigus sellele sõlmele administraatori sõnumeid saata. - Väljunda reaalajas arendajalogi jadapordi kaudu, vaata ja salvesta asukoha redigeerimisega seadmelogisid sinihamba ​​kaudu. - Seadet haldab kärgvõrgu administraator, kasutajal pole juurdepääsu seadme sätetele. Kasutatakse jagatud võtme loomiseks kaugseadmega. Seade ei jaga oma privaatvõtit kaughalduse kaudu. Saate määrata uue võtme, kuid seda ei saa kunagi tagasi lugeda. Genereeritakse privaatvõtmest ja saadetakse kärgvõrgu sõlmedele, et nad saaksid koostada jagatud salajase võtme. - Jadapordi konsool voog API kaudu. Sätted Sinihamba ​​õiguste sätted Kriitiliste hoiatuste seadistamine @@ -357,7 +308,6 @@ Filter kaasa arvatud Eelseadistatud filtrid Filtrid - Silumislogi API lubatud Rakenduste logisid pole kuvada Värskenda Salvesta logi @@ -406,8 +356,6 @@ Üksikasjad Tuvastusandur Tuvastusanduri sätted - Tuvastusandur lubatud - Identifitseerimistüüp Seade Seadme sätted @@ -421,17 +369,15 @@ Seadme mõõdikud %1$s %1$s: %2$s%% - Seadme mõõdikute värskendamise intervall %1$s: %2$s V Seade on unerežiimis Seadme salvestusruum & UI (kirjutuskaitstud) - Saada seadme telemeetria - Luba/keela seadme telemeetriamoodulil andmete saatmine kärgvõrku. Need on nimiväärtused. Ülekoormatud kärgvõrgu puhul skaleeritakse need automaatselt pikemale intervallile olenevalt võrgus olevate sõlmede arvule. Teema: %1$s, Keel: %2$s Kastepunkt Otsesõnum Otsesõnumi võti Otsesõnumid + Keelatud Tühista Katkesta ühendus Ühendus katkenud @@ -508,22 +454,16 @@ Peata skaneerimine TI analüüs pole saadaval %1$s jäänud - %1$d unikaalset sõlme Vaata kaarti Vaba kettamaht %1$d - Ekraan Seadme ekraan - Ekraani režiim - Kui see on lubatud, kuvab seade ekraanil aega 12 tunni formaadis. - Ekraani ühikud Kaugus Kauguse filter Filtreeri sõlmede loendit ja kärgvõrgustiku kaarti kauguse põhjal sinu telefonist. Kauguse mõõtmised Kuva telefoni ja teiste Meshtastic sõlmede asukoha vaheline kaugus. - DNS Puhasta otsing süsteemi ti, gemini, assistent, funktsioonid, automatiseerimine, hääl @@ -564,6 +504,7 @@ MQTT Sõlme mõõdikud Sõlmed + Märguanded Alustamine Sätted — Moodulid & Administraator Sätted — Raadio & Kasutaja @@ -576,14 +517,13 @@ Dokumentatsioon Valmis Ära selle seadme puhul enam kuva - Topeltpuudutus nupuna MQTT allalaadimine lubatud Avaliku interneti ligipääsu sõnumid edastatakse kohalikku kärgvõrgu. Nullhüppe poliitika tõttu ei levi MQTT vaikeserverist tulev liiklus sellest seadmest kaugemale. Lae alla Tuvastati korduv avalik võti + Dünaamiline Loo hõlpsalt privaatseid kärgvõrke turvaliseks ja usaldusväärseks suhtluseks asustamata piirkondades. - Kaja lubatud Muuda Muuda võrgupaani allikat 8 tundi @@ -600,27 +540,18 @@ Ei saa emoji laadida Emoji ei leita Viimati kasutatud - Luba energiasäästurežiim Lubatud - - Krüpteerimine lubatud Kokkusobimatu avalik võti Avalikvõti ei ühti salvestatud võtmele. Võid sõlme eemaldada ja lasta uuesti võtmeid vahetada, kuid see võib viidata tõsisemale turvaprobleemile. Võtke kasutajaga ühendust mõne muu usaldusväärse kanali kaudu, et teha kindlaks, kas võtmevahetus oli tingitud tehaseseadete taastamisest või muust tahtlikust toimingust. Avaliku võtme krüpteerimine Keskkonnamõõdikud - Keskkond - Keskkonnamõõdikute lubamine - Keskkonnamõõdikute ekraanil kuvamine lubatud - Keskkonnamõõdikute värskendamise intervall - Keskkonnamõõdikud kasutavad Fahrenheiti Viga Töötsükli limiit on saavutatud. Sõnumite saatmine ei ole hetkel võimalik. Proovi hiljem uuesti. Pärast korduvaid katseid ei õnnestunud stabiilset ühendust luua. Uuesti proovimiseks vali palun sõlm. Ühenda & ja halda Kaugühenduse loomine… Etherneti valikud - Ethernet lubatud Etherneti IP-aadress: Sündmuste püsivara käivitamine Kasuta sündmuse teemat @@ -636,7 +567,6 @@ Salvesta TAK andmepakett Välised teated Välisete teadete sätted - Luba Välised teated Tehasesätted Rahuldav Meshtastic %1$s @@ -647,10 +577,6 @@ Saadaval failid (%1$d): Lisa sõna või regulaaravaldise:muster - Keela filtreerimine - Luba filtreerimine - Luba filtreerimine - Peida filtrisõnu sisaldavad sõnumid Peida %1$d filtreeritud Filtreeri Filtreeritud @@ -759,6 +685,7 @@ See võib võtta minuti... Sihtkoht: %1$s Püsivara uuendus + %1$d% Tundmatu viga Tundmatu riistvaramudel: %1$d Tundmatu väljalase @@ -779,9 +706,7 @@ Tühjendada seade uuendamise käigus Puhastab seadme välkmälu täielikult ja paigaldab seejärel valitud püsivara nullist. Püsivara versioon: %1$s - Fikseeritud PIN Määratud asukoht - Keera ekraani Lisateabe saamiseks vaata privaatsuspoliitikat. Paks @@ -792,13 +717,7 @@ Vaba mälumaht Saadaolev süsteemimälu baitides Sagedus - Sageduspesa - Kasutajasõbralik nimi Gaasi surve - Lüüs - Sisendsündmus CCW pordis - Sisendsündmus CW pordis - Sisendsündmus Vajuta pordis Genereeri QR kood Geopiire @@ -823,31 +742,17 @@ Algusesse GitHubi hoidla Hea - GPIO GPIO klemm - GPIO klemmi pöördvalik A pordis - GPIO klemmi pöördvalik B pordis - GPIO klemmi pöördvalik Vajuta pordis - GPIO klemmi jälgimine - GPS EN GPIO - GPS-režiim (riistvara) - GPS vastuvõtu GPIO - GPS saatmise GPIO Luba juurepääs - Roheline Raudvara Seadme mudel Päis - Südamelöögid Abi & Dokumentatsioon Peida kiht Peida parool - Ajalookirjete maksimaalne arv - Ajalookirjete aken Selles akna sõlmedest pole ühtegi kuulda Sõlme hüppe kohta - Hüpete arv Hüppe kaugusel Host Hosti mõõdik @@ -855,18 +760,12 @@ Nõustun. Olen ülaltoodu läbi lugenud ja sellest aru saanud. Annan vabatahtlikult nõusoleku oma sõlmeandmete krüpteerimata edastamiseks MQTT kaudu Ma tean mida teen. - I2S kell - I2S info sisse - I2S info välja - I2S sõna valik IAQ Siseõhu kvaliteet (IAQ) on suhtelise skaala väärtus, näiteks mõõtes Bosch BME680 abil. Väärtuste vahemik 0–500. Ikooni tähendused - Eira Lisa '%1$s' eiramis loendisse? Ignoreeri sissetulevaid - Keela MQTT Eemaldada '%1$s' eiramis loendist? Lae sätted @@ -894,7 +793,6 @@ IP IP-aadress: Port: - IPv4 režiim JSON väljund lubatud %1$s %1$s +%2$d @@ -917,8 +815,6 @@ Katvuse hinnang Võrgukiht Lisateave - Südamelöögi LED - LED olek Pärandadministraatori kanal %1$d teek @@ -1013,13 +909,10 @@ Luksi Halda kohandatud kardikihti Halda kaardikihte - Hallatud režiim Vajalik käsitsi asukohapäring Kärgvõrgu kaart - Vahemälu maht: %1$d MB\nVahemälu kasutus: %2$d MB Vahemälu haldamine - %1$s MB Praegune vahemälu suurus %1$d paani Tühjenda allalaetud paanid @@ -1040,11 +933,8 @@ Ilmaradar SQL-i vahemälu tühjendamine ebaõnnestus, vaata üksikasju logcat'ist SQL-i vahemälu puhastatud %1$s' jaoks - Kaardi raport Nõusolek krüpteerimata sõlmeandmete jagamiseks MQTT kaudu Selle funktsiooni lubamisega kinnitate ja nõustute selgesõnaliselt oma seadme reaalajas geograafilise asukoha edastamisega MQTT protokolli kaudu ilma krüpteerimiseta. Neid asukohaandmeid võidakse kasutada sellistel eesmärkidel nagu: reaalajas kaardi aruandlus, seadme jälgimine ja seotud telemeetriafunktsioonid. - Kaardi raporti sagedus (sekund) - Teie sõlm saadab pidevalt määratletud MQTT serverile krüpteerimata kaardiaruande pakete, mis sisaldab ID-d, pikka- ja lühinime, ligikaudset asukohta, riistvaramudelit, rolli, püsivara versiooni, LoRa regiooni, modemi eelseadistust ja peamise kanali nime. Vali allalaetav piirkond Alusta allalaadimist Kaardi stiilis valik @@ -1069,7 +959,6 @@ Kärgvõrgu kutsed Kuula majakaid Lähedalasuvate kärgvõrkude reklaamitud kutsete jäädvustamine - Majaka sõnum Maksimum %1$d baiti Lähedal asuv kärgvõrk kutsus liituma Kärgvõrgu kutsed @@ -1156,9 +1045,6 @@ %1$s %2$d µg/m³ Min - Minimaalne edastusaeg (sekund) - Nutikas kaugus - Nutikas intervall Minimaalne ärkveloleku aeg Eelseadistused Mooduli sätted @@ -1168,7 +1054,6 @@ MQTT MQTT sätted - MQTT lubatud MQTT: ühendus katkes MQTT: ühendus lükati tagasi (kontrolli volitusi) MQTT puhverserveri töö ebaõnnestus: %1$s @@ -1182,8 +1067,6 @@ Vahendajaga ei saa ühendust (TCP) Ajaline katkestus peale %1$d ms TLS kätlemine ebaõnnestus: %1$s - Selle telefoni MQTT puhverserver - Telefon vahendab ühendatud seadme MQTT-liiklust. Lülita välja, et vahendaminee katkestada - ilma seadme MQTT-sätet muutmata. See on kasulik, kui MQTT-liiklus ühenduse üle koormab. Vahendamise jätkamiseks lülita see uuesti sisse. Ühendatud Ühendan… Ühendus katkenud @@ -1206,7 +1089,6 @@ Vaigistatud %1$d päeva, %2$s tundi Vaigistatud %1$s tundi Mitte vaigistatud - Häire ajalõpp (sekundit) Nimi Nimi ei tohi olla tühi. Liigu tagasi @@ -1216,7 +1098,6 @@ Meshtastic vajab ühilduvat seadet. Meie toetajad ja partnerid pakuvad kasutusvalmis riistvara. Siin on mõned populaarseimad valikud. Naabruse teave Naabruskonna teabe sätted - Naabruskonna teave lubatud Võrk Uued kanalid vastu võetud Uued sõnumid allpool @@ -1229,7 +1110,6 @@ Sinihamba-seadmeid ei tuvastatud Kohandatud paanide allikaid ei leitud. Seadet pole valitud - Ühtegi seadet ei leitud Faile ei avaldatud. Statistikat pole saadaval Kaardikihte pole laetud. @@ -1291,7 +1171,6 @@ läbi Lemmikud läbi MQTT NodeDB lähtestamine - Sõlme teabe edastamise intervall Sõlmed Sõlmed siin asukohas @@ -1310,6 +1189,8 @@ Mitte praegu Märkus Sõnumid + + Kärgvõrk Meshtastic kasutab märguandeid, et hoida teid kursis uute sõnumite ja muude oluliste sündmustega. Saate oma märguannete õigusi igal ajal seadetes muuta. Kanali märguanded ja otsesõnumid. @@ -1318,13 +1199,9 @@ Hoiatuskella vastuvõtmise teated Sõnumi vastuvõtmise teated Praegu - NTP server - Kirjete arv Võrguühenduseta kaardid - Ok MQTTi Olgu - OLED tüüp 24 tundi 1 tund @@ -1342,23 +1219,13 @@ Ava WiFi ​​seaded Valikud Põhja suund - - Väljund summer (GPIO) - Väljundi kestvus (millisekundit) - Väljund LED aktiivne - Väljund LED (GPIO) - Väljund värin (GPIO) Lisa valikud Konsooli jadapordi alistamine - Töötsükli tühistamine - Tühista sagedus - PA ventilaator keelatud Paketi autentsus Tasakaalustatud — eelista autentitud Soovitatav. Keeldu allkirjastamata madalamale versioonile ülemineku katsetest sõlmedelt, mis teadaolevalt allkirjastavad. Ühildub – aktsepteeri allkirjastamata - Autentige pakette võimaluse korral, kuid maksimaalse ühilduvuse tagamiseks aktsepteerige allkirjastamata liiklust. Kaitsetase Range – nõuab autentimist Luba range @@ -1366,7 +1233,6 @@ Kuva ja töötle ainult krüptograafiliselt autentitud kärgvõrgu pakette. Vanemad sõlmed ja ülisuured paketid võivad kaduda. Kas lubada range autentimine? See ühendatud seade ei toeta pakettallkirja kontrollimist. - Sidumine Parool PAX @@ -1379,7 +1245,6 @@ Wi-Fi: %1$s Pax loendur Paxcounter sätted - Paxcounter lubatud Perioodiline asukoha jagamine Leia raadiod sinu WiFi võrgust @@ -1414,6 +1279,7 @@ %1$d sekund %1$d sekundit + PM1,0 PM10 PM2,5 @@ -1421,16 +1287,11 @@ Asukoht Kasuta telefoni hetkelist asukohta Asukoht lubatud - Asukoha lipp Asukoht Asukoha pakett - Toide Toite sätted Võimsusnäitajad - Toitemõõdiku moodul on lubatud - Toitemõõdiku ekraanil kuvamine lubatud - Toitemõõdikute värskendamise intervall Toitega ppm Täpne asukoht @@ -1452,13 +1313,10 @@ Tekst Peamine Pidev asukoha ja telemeetria edastamine - Salajane võti Projekti teave Jaga telefoni asukohta mesh-võrku Teenusepakkuja nimi on olemas. - Kliendi proksi lubatud PSK - PTT klemm Avalik võti Avalik võti muudetud QR kood @@ -1476,25 +1334,14 @@ Vihm (24h) Ulatustest Ulatustesti sätted - Ulatustest lubatud Reageeri Taaskäivita - - Kordusülekannete režiim - Saada uuesti mis tahes jälgitav sõnum, kui see oli privaatkanali või teisest samade LoRa parameetritega kärgvõrgus. - Sama käitumine nagu KÕIK puhul, aga pakete ei dekodeerita vaid need lihtsalt edastatakse. Saadaval ainult Repiiteri rollis. Teise rolli puhul toob kaasa KÕIK käitumise. - Ignoreerib pakette mittestandardsetest pordinumbritest, näiteks: TAK, RangeTest, PaxCounter jne. Edastatakse ainult standardsete pordinumbritega pakette: NodeInfo, Tekst, Asukoht, Telemeetia ja Routimine. - Ignoreerib sõnumeid välistest kärgvõrkudest, nt AINULT KOHALIK, saadud sõnumeid, kuid läheb sammu edasi, ignoreerides ka sõnumeid sõlmedelt, mis pole veel tuntud sõlme loendis. - Ignoreerib jälgitavaid sõnumeid välis kärgvõrkudest avatud või dekrüpteerida mittevõimalikke sõnumeid. Saadab sõnumeid ainult kohalikel primaarsetel/sekundaarsetel kanalitel. - Lubatud ainult rollidele SENSOR, TRACKER ja TAK_TRACKER, see blokeerib kõik kordus edastused, vastupidiselt CLIENT_MUTE rollile. Hiljuti nähtud seadmed Taas ühendan… - Punane Värskenda Värskenda metaandmeid Kas olete kindel, et soovite oma privaatvõtit uuesti luua?\n\nSõlmed, mis võisid selle sõlmega varem võtmeid vahetanud, peavad turvalise suhtluse jätkamiseks selle sõlme eemaldama ja võtmed uuesti vahetama. Loo uus privaatvõti - Regioon Kuuldud vahendaja %1$d Kuuldud %1$d vahendajat @@ -1545,31 +1392,17 @@ Seadme roll Klient Klient-baas - Käsitleb lemmiksõlmedest tulevaid või neile saadetud pakette kui RUUTER_HILINE ja kõiki teisi pakette kui KLIENT. - Rakendusega ühendatud või iseseisev sõnumsideseade. Peidetud klient - Seade, mis edastab ülekandeid ainult siis, kui see on vajalik varjamiseks või energia säästmiseks. Vaikne klient - Seade, mis ei edasta pakette teistelt seadmetelt. Kaotud ja leitud Repiiter - Infrastruktuuri sõlm võrgu leviala laiendamiseks, edastades sõnumeid minimaalse üldkuluga. Pole sõlmede loendis nähtav. Ruuter Ruuteri klient - Ruuteri ja Kliendi kombinatsioon. Ei ole mõeldud mobiilseadmetele. - Infrastruktuuri sõlm võrgu leviala laiendamiseks sõnumite edastamise kaudu. Nähtav sõlmede loendis. Hiline ruuter - Infrastruktuurisõlm, mis saadab pakette ainult ühe korra, ning alles peale kõiki teisi sõlmi, tagades kohalikele klastritele täiendava katvuse. Nähtav sõlmede loendis. Andur - Esmalt edastatakse telemeetria pakette. TAK - Optimeeritud ATAK süsteemi side jaoks, vähendab rutiinseid saateid. Jälgitav TAK - Võimaldab automaatseid TAK PLI saateid ja vähendab rutiinseid saateid. Jälgitav - Esmajärjekorras edastatakse GPS asukoha pakette. - Juurteema - Pöördvalik #1 lubatud Olen lugenud <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Seadme rolli dokumenti</a> ja blogi postitust <a href="http://meshtastic.org/blog/choosing-the-right-device-role"> Kuidas valida seadmele õige roll</a>. Admin sessioon aegunud @@ -1592,13 +1425,10 @@ Sõnum on saatmiseks liiga pikk RSSI Vastuvõetud signaali tugevuse indikaator (RSSI), mõõt mida kasutatakse antenni poolt vastuvõetava võimsustaseme määramiseks. Kõrgem RSSI väärtus näitab üldiselt tugevamat ja stabiilsemat ühendust. - rsyslog server Sateliit - Salvesta Salvesta & taaskäivitus Salvesta - Salvesta .CSV faili (ainult ESP32) Ekspordi rangetest paketid Otsi @@ -1612,7 +1442,6 @@ Skanneeri jagatud kontakt QR kood Otsin… Otsin… - Ekraan sisse lülitatud Mine lõppu Otsi emotikone... Otsi sõnumeid… @@ -1645,19 +1474,8 @@ Valitud Vali kaardi tüüp Saada - Saada Kõll - Saada kõll koos hoiatussõnumiga - Saatja sõnumi sagedus (sekundit) - Jadaport - Jadapordi kiirus Jadapordi sätted - Jadapordi konsool - Jadaport lubatud - Jadapordi režiim - RX - TX - Server Sessioon aktiivne Taaskäivitus vajalik Määra aeg @@ -1688,7 +1506,6 @@ Kuva teekonnapunktid Lülita välja Sõlm: %1$s - Väljalülitamine voolukatkestuse korral ⚠️ See LÜLITAB sõlme välja. Uuesti sisselülitamiseks on vaja füüsilist sekkumist. Levi Levi Kvaliteet @@ -1722,7 +1539,6 @@ Süsteemi WebView värskendatakse. Palun proovi hetke pärast uuesti. Jäta vahele Pesa - Nutikas asukoht SNR Signaali ja müra suhe (SNR) on mõõdik, mida kasutatakse soovitud signaali taseme ja taustamüra taseme vahelise suhte määramisel. Meshtastic ja teistes traadita süsteemides näitab kõrgem signaali ja müra suhe selgemat signaali, mis võib parandada andmeedastuse usaldusväärsust ja kvaliteeti. Pinnase niiskus @@ -1730,16 +1546,11 @@ Kiirus %1$d Km/h %1$d mph - Levitustegur - SSID - Oleku edastus (sekund) Oleku teavitus Igal pool ühenduses Peata ühendamine Salvesa & edasta Salvesta & edasta sätted - Salvesta & edastamine lubatud - Alamvõrk Edukas Super sügava une kestus Toetatud @@ -1747,21 +1558,10 @@ Eemalda Vaigista Eemalda vaigistus - RX võimendatud võimendus Süsteemi sätted TAK (ATAK) TAK-i sätted - Liikme roll - Luure - Peakorter - Koer (K9) - Meedik - Sidemees - Snaiper - Meeskonna ülem - Meeskonnaliige - Määramata TAK server TAK kärgvõrgu kanal Väljuva TAK liikluse jaoks kasutatakse Meshtastic kanalit @@ -1795,22 +1595,6 @@ Käivita Töötav: %1$s Ühendatud sõlme püsivara ei toeta täielikku TAK integratsiooni – ATAK-iga ühendatakse ainult asukoha- ja vestlussõnumid. Markerite ja muud tüüpi sündmuste jaoks on vaja püsivara versiooni 2.8.0 või uuemat. - Meeskonna värv - Sinine - Pruun - Tsüaan - Tume sinine - Tume roheline - Roheline - Fukspunane - Kastanpruun - Oranž - Lilla - Punane - Sinakasroheline - Määramata - Valge - Kollane Telemeetria Telemeetria sätted Temperatuur @@ -1819,10 +1603,8 @@ Hele Süsteemi vaikesäte Aeg - Ajavöönd Aegunud Ajatempel - TLS lubatud Lülita asukoht sisse Trace Route @@ -1872,7 +1654,6 @@ Sõnumit ei õnnestunud tõlkida Tõlkemudeli allalaadimine ebaõnnestus Sõnum on juba teie keeles - Saada LoRa kaudu Transport API @@ -1886,8 +1667,6 @@ 24T 48 tundi 2N - Edastus lubatud - Saatevõimsus Tüüp Sisesta sõnum UDP levitamine @@ -1911,9 +1690,6 @@ Eemalda Tundmatu Tühistatud - 0 - Üles/Alla/Vali sisend lubatud - GPS-i küsimise intervall - Uuenduste sagedus (sekundit) Uuendatud MQTT üleslaadimine lubatud Võrgusõlme sõnumid saadetakse avalikku internetti mis tahes sõlme konfigureeritud ligipääsu kaudu. @@ -1926,13 +1702,7 @@ URL mall USB USB luba keelatud. Ühenda seade uuesti ja proovi uuesti. - - Kasuta 12 tunni formaati Kompaktne kodeering kirillitsa jaoks - Kasuta I2S summerina - Kasuta INPUT_PULLUP režiimi - Kasuta eelseadistust - Kasuta PWM summerit Kasutaja Kasutaja sätted @@ -1940,7 +1710,6 @@ Kasutaja teave Kasutaja string Kasutaja teave - Kasutajatunnus UV Luks API kaudu läbi MQTT @@ -1948,8 +1717,6 @@ Vaata kaardil Näita versioon Vool - Oota Bluetoothi ​​kestust - Ärata puudutusega või liigutusega Hoiatus Eemalda teekonnapunkt? Muuda teekonnapunkti @@ -1962,7 +1729,6 @@ Wi-Fi valikud WiFi ühenduse loomine mPWRD-OS-i jaoks - Wi-Fi lubatud Wi-Fi IP: Saada olevad võrgud Ühenduse loomine ebaõnnestus: %1$s diff --git a/core/resources/src/commonMain/composeResources/values-fi/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-fi/schema_strings.xml new file mode 100644 index 0000000000..8f44b3e2be --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-fi/schema_strings.xml @@ -0,0 +1,295 @@ + + + + + Sininen + Virta + Vihreä + LED-tila + Punainen + Oletus + CODEC2 näytteenottotaajuus + CODEC 2 käytössä + I2S-datalähtö + I2S-kello + I2S-datatulo + I2S-sanan valinta + PTT-pinni + Bluetooth käytössä + Paritustila + Luo syötetapahtuma vastapäivään käännettäessä + Luo syötetapahtuma myötäpäivään käännettäessä + Luo syötetapahtuma painettaessa + GPIO-pinni kiertovalitsinta varten A-portti + GPIO-pinni kiertovalitsinta varten B-portti + GPIO-pinni kiertovalitsimen painallusportille + Edellinen + Peruuta + ei mitään + Valitse + Kiertovalitsin #1 käytössä + Lähetä äänimerkki + Ylös/Alas/Valitse syöte käytössä + Tunnistuksen tyyppi + Tunnistinsensori käytössä + GPIO-pinni valvontaa varten + Käyttäjäystävällinen nimi + Lähetä äänimerkki hälytyssanoman kanssa + Korkea + Käytä INPUT_PULLUP tilaa + Painikkeen GPIO-pinni + Summerin GPIO-pinni + Poista kolmoisklikkaus käytöstä + Kaksoisnapautus painikkeena + Käsittele tuetun kiihtyvyysanturin kaksoisnapautusta käyttäjäpainikkeella. + Ledin valvontasignaali + Laitteen tietojen lähetyksen aikaväli + Uudelleenlähetyksen tila + Kaikki + Ohita kaikki dekoodaukset + Käyttäytyy samalla tavalla kuin ALL, mutta jättää pakettien purkamisen väliin ja lähettää niitä vain uudelleen. Mahdollista käyttää vain Repeater-roolissa. Tämän asettaminen muille rooleille johtaa ALL-käyttäytymiseen. + Ainoastaan ytimen porttinumerot + Vain tunnetut + Vain paikallinen + Ei ota huomioon havaittuja viestejä ulkomaisista verkoista, jotka ovat avoimia tai joita se ei voi purkaa. Lähettää uudelleen viestin vain laitteen paikallisilla ensisijaisilla / toissijaisilla kanavilla. + ei mitään + Sallittu vain SENSOR-, TRACKER- ja TAK_TRACKER -rooleille. Tämä estää kaikki uudelleenlähetykset, toisin kuin CLIENT_MUTE -roolissa. + Laitteen rooli + Client + Client Base + Client Hidden + Laite, joka lähettää vain tarvittaessa tai virransäästotilassa. + Client Mute + Laite, joka ei välitä paketteja muilta laitteilta. + Lost and Found + Lähettää laitteen sijainnin viestillä oletuskanavalle sen löytämisen helpottamiseksi. + Repeater + Router + Router Client + Router Late + Sensor + Lähettää telemetriatiedot ensisijaisesti. + TAK + Optimoitu ATAK-järjestelmän viestintään, joka vähentää tavanomaisia lähetyksiä. + TAK Tracker + Ottaa käyttöön automaattisen TAK PLI -lähetyksen vähentäen tavanomaisia lähetyksiä. + Tracker + Lähettää GPS-sijaintitiedot ensisijaisesti. + Aikavyöhyke + Karusellin aikaväli + Vaihtaa automaattisesti seuraavalle näytön sivulle kuin karuselli, määritetyn aikavälin perusteella. + Osoita aina pohjoiseen + Näytön ympyrän ulkopuolella oleva kompassisuunta osoittaa aina pohjoiseen. + Kompassin suuntaus + Näyttötila + Ohita oletusnäytön asettelu. + Tuumajärjestelmä + Metrijärjestelmä + Käännä näyttö + Käännä näyttö pystysuunnassa. + Lihavoitu otsikko + Tee näytön otsikkotekstistä lihavoitu. + OLED-tyyppi + Ohita OLED-näytön automaattinen tunnistus. + Näytön päälläoloaika + Kuinka kauan näyttö pysyy päällä sen jälkeen, kun käyttäjäpainiketta on painettu tai viestejä on vastaanotettu. + Näyttöyksiköt + Laitteen näytöllä näytettävät yksiköt. + Käytä 12 tunnin kelloa + Kun asetus on käytössä, laite näyttää 12 tunnin ajan näytössä. + Herätä napautuksesta tai liikkeestä + Edellyttää, että laitteessasi on kiihtyvyysanturi. + Ulostulon LED aktiivinen + Hälytysäänen LED + Hälytysäänen äänimerkki + Hälytysäänen värinä + Hälytysviestin LED + Hälytysviestin äänimerkki + Hälytysviestin värinä + Ulkoiset ilmoitukset käytössä + Toistokehotuksen aikakatkaisu + Ulostulon LED (GPIO) + Ulostulon äänimerkki (GPIO) + Ulostulon värinä (GPIO) + Käytä I2S protokollaa äänimerkille + Käytä PWM-äänimerkkiä + Kaistanleveys + Taajuuspaikka + Laitteesi käyttämä taajuus lasketaan alueen, modeemin esiasetuksen ja tämän kentän perusteella. Kun arvo on 0, aikaväli lasketaan automaattisesti ensisijaisen kanavan nimen perusteella ja se poikkeaa oletus-julkisesta aikavälistä. Vaihda takaisin julkiseen oletus-aikaväliin, jos käytössä on yksityinen ensisijainen ja julkinen toissijainen kanava. + Koodausnopeus + MQTT päällä + Hyppyjen määrä + Asettaa maksimihyppyjen määrän, oletus on 3. Hyppyjen määrän lisääminen kasvattaa myös ruuhkaa, joten sitä tulisi käyttää varoen. 0 hypyn lähettämät viestit eivät saa kuittauksia (ACK). + Ohita MQTT + Esiasetukset + Lite - Fast + Lite - Slow + Long Range - Fast + Long Range - Moderate + Long Range - Slow + Long Range - Turbo + Medium Range - Fast + Medium Range - Slow + Medium Range - Turbo + Narrow - Fast + Narrow - Slow + Short Range - Fast + Short Range - Slow + Short Range - Turbo + Tiny - Fast + Tiny - Slow + Very Long Range - Slow + Ohita käyttöaste (Duty Cycle) + Taajuuden ohitus + PA tuuletin pois käytöstä + Alue + Alue, jossa aiot käyttää radiolaitteitasi. + Levennyskerroin (Spread Factor) + RX tehostettu vahvistus + Lähetys käytössä + Lähetysteho + Käytä esiasetusta + Havaitsija etulinjassa + Lääkäri + Tarkka-ampuja + Joukkueen johtaja + Tiimin jäsen + Viesti + Osoite + MQTT käytössä + Salaus käytössä + Karttaraportointi + Laitteesi lähettää säännöllisin väliajoin salaamattoman karttaraporttipaketin määritettyyn MQTT-palvelimeen. Paketti sisältää tunnisteen, lyhyen ja pitkän nimen, likimääräisen sijainnin, laitteistomallin, roolin, laiteohjelmistoversion, LoRa-alueen, modeemin esiasetukset sekä ensisijaisen kanavan nimen. + Salasana + Välityspalvelin käytössä + Palvelimen osoite (root topic) + TLS käytössä + Käyttäjänimi + Naapuritiedot käytössä + Lähetä LoRa:n kautta + Lähetetäänkö naapuritiedot LoRa:n kautta sen lisäksi, että ne lähetetään MQTT-protokollalla ja PhoneAPI sovellusrajapinnassa? Tämä ei ole tuettu kanavalla, joka käyttää oletussalausavainta ja nimeä. + GPS-kyselyn aikaväli + IPv4-tila + Ota käyttöön pakettien lähettäminen UDP:n kautta paikallisverkossa. + Ethernet käytössä + Ethernetin ottaminen käyttöön poistaa Bluetooth-yhteyden sovellukseen. TCP-laiteyhteydet eivät ole käytettävissä Applen laitteilla. + DNS + Yhdyskäytävä + IP + Aliverkko + NTP palvelin + ei mitään + UDP-lähetys + rsyslog-palvelin + WiFi käytössä + WiFi-yhteyden käyttöönotto katkaisee Bluetooth-yhteyden sovellukseen. + Salasana + SSID + BLE-signaalin RSSI-kynnysarvo (oletus -80) + PAX-laskuri käytössä + GPS-kyselyn aikaväli + WiFi-signaalin RSSI-kynnys (oletus: -80) + Älykäs etäisyys + Vähimmäisetäisyys metreinä, jonka muutos otetaan huomioon älykkäässä sijainnin lähetyksessä. + Älykäs aikaväli + Kiinteä sijainti + GPS EN GPIO-pinni + GPS-tila (fyysinen laitteisto) + GPS-kyselyn aikaväli + Kuinka usein yritetään hakea GPS-sijainti (<10 sekuntia pitää GPS:n päällä). + Ei käytössä + Käytössä + Lähetyksen aikaväli + Suurin aikaväli, jonka aikana laite ei lähetä sijaintia. + Älykäs sijainti + Sijaintimerkinnät + Valinnaiset kentät, jotka sisällytetään sijaintiviesteihin. Mitä enemmän kenttiä sisällytetään, sitä suurempi viesti on, mikä pidentää lähetysaikaa ja lisää pakettihäviön riskiä. + Korkeus + Korkeus on mitattu merenpinnan tasosta + Korkeuden geoidinen erotus + Ajoneuvon suunta + Satelliittien määrä + Sekvenssinumero + Ajoneuvon nopeus + Aikaleima + GPS vastaanoton GPIO-pinni + GPS lähetyksen GPIO-pinni + Korvaava AD-muuntimen kerroin + Ota virransäästötila käyttöön + Asetus laittaa kaiken mahdollisen lepotilaan. Seuranta- ja anturiroolissa tämä sisältää myös LoRa-radion. Älä käytä tätä asetusta, jos haluat käyttää laitetta puhelinsovellusten kanssa tai laitetta ilman käyttäjäpainiketta. + Sammuta virran katketessa + Bluetoothin odotusaika + Kuuluvuustesti käytössä + Tallenna .CSV (ESP32 ainoastaan) + Ylläpitäjän avain + Julkinen avain, jolla on oikeus lähettää hallintaviestejä tälle laitteelle. + Vianetsintälokirajapinta käytössä + Tulosta reaaliaikainen virheenkorjausloki sarjaportin kautta, ja tarkastele sekä vie Bluetoothin kautta laitteesta poistettuja sijaintitietoja sisältäviä lokitiedostoja. + Hallintatila + Laite on verkon ylläpitäjän hallinnoima, eikä käyttäjä pääse muokkaamaan laitteen asetuksia. + Yksityinen avain + Käytetään jaetun avaimen luomiseen etälaitteen kanssa + Julkinen avain + Sarjaporttikonsoli + Sarjaporttikonsoli käyttöön Stream API:n kautta. + Sarjaportin nopeus + Sarjaportin nopeus + Palautus päällä + Sarjaportti käytössä + Sarjaportin tila + RX + Oletus + Oletus + Aikakatkaisu + TX + Varastoi & välitä käytössä + Valvontasignaali + Historian maksimimäärä + Historian aikamäärä + Palvelin + Kirjausten määrä + Rooli + Tiimi + Sininen + Ruskea + Turkoosi + Tummansininen + Tummanvihreä + Vihreä + Purppura + Viininpunainen + Oranssi + Liila + Punainen + Sinivihreä + Valkoinen + Keltainen + Ilmanlaadun tietojen moduuli käytössä + Ilmanlaatumittareiden päivitysväli + Lähetä laitteen telemetriatiedot + Ota käyttöön / poista käytöstä laitteen telemetriamoduuli, joka lähettää mittaustietoja mesh-verkkoon. Nämä ovat nimellisiä (oletus) arvoja. Ruuhkautuneissa mesh-verkoissa lähetysväli pitenee automaattisesti verkossa olevien (online) solmujen määrän perusteella. + Laitemittareiden päivitysväli + Käytä Fahrenheit yksikköä + Ympäristötietojen moduuli käytössä + Näytä ympäristötiedot näytöllä + Ympäristömittareiden päivitysväli + Virrankulutuksen moduuli käytössä + Virrankulutuksen näyttö käytössä + Virtamittareiden päivitysväli + Tuntemattomien pakettien kynnysarvo + diff --git a/core/resources/src/commonMain/composeResources/values-fi/strings.xml b/core/resources/src/commonMain/composeResources/values-fi/strings.xml index 8ff5e642f4..7d9f6bee0c 100644 --- a/core/resources/src/commonMain/composeResources/values-fi/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fi/strings.xml @@ -17,18 +17,20 @@ --> + Kosteus %1$s: %2$s Viesti käyttäjältä %1$s: %2$s akku %1$d % + Kanava %1$d %1$s pois suosikki - %1$d hypyn päässä viimeksi kuultu %1$s offline-tilassa online-tilassa rooli %1$s signaali %1$s Hyppy %1$d: %2$d radiota + Lämpötila Tietoja Hyväksy Kiitokset @@ -46,7 +48,6 @@ Käännä viesti Toiminnot ADC-kertoimen ohitus - Korvaava AD-muuntimen kerroin ADC-jännite Lisää @@ -60,7 +61,6 @@ Lisää laite manuaalisesti… Lisää verkkokarttataso Osoite - Ylläpitäjän avain Ylläpitäjän avaimet Ylläpito Lisäasetukset @@ -70,24 +70,14 @@ Ilmanlaatu Ilmanlaadun kuvake Ilmanlaatumittausten loki - Ilmanlaadun tietojen moduuli käytössä - Ilmanlaatumittareiden päivitysväli Viimeisen tunnin aikana käytetyn lähetyksen prosenttiosuus. Lähetysajan käyttöaste - - Hälytysäänen äänimerkki - Hälytysäänen LED Hälytysääni! - Hälytysäänen värinä - Hälytysviestin äänimerkki - Hälytysviestin LED - Hälytysviestin värinä Kaikki Salli syötteen lähde Salli määrittämättömän pinnin käyttö Korkeus Korkeus - Osoita aina pohjoiseen Ympäristövalaistus Ympäristövalaistuksen asetukset Analytiikkatietoja kerätään auttamaan meitä parantamaan Android-sovellusta (kiitos siitä). Saamme anonymisoitua tietoa käyttäjien toiminnasta, kuten kaatumisraportteja ja tietoa sovelluksessa käytetyistä näkymistä jne. @@ -135,8 +125,6 @@ Tallentaa julkiset ja yksityiset avaimet tämän laitteen suojattuun, salattuun tallennustilaan. Varmuuskopioi & palauta Huono - - Kaistanleveys Oletus (%1$s kHz) %1$s kHz Ei tuettu (%1$s) @@ -145,15 +133,12 @@ Akku INA_2XX-akun valvontapiirin I2C-osoite Bluetooth-laitteet - BLE-signaalin RSSI-kynnysarvo (oletus -80) Bluetooth-laitteiden haku edellyttää tässä Android-versiossa myös sijaintipalvelujen käyttöä. Sijaintiasi ei käytetä. - Sininen Bluetooth Saatavilla olevat Bluetooth-laitteet Bluetooth asetukset Bluetooth on pois käytöstä. Ota se käyttöön etsiäksesi lähellä olevia laitteita. - Bluetooth käytössä Asetukset Hallitse laitteesi asetuksia ja kanavia langattomasti. Haku @@ -174,16 +159,12 @@ Bluetooth-haun enimmäismäärä on saavutettu. Yritä uudelleen %1$d sekunnin kuluttua. Bluetooth-haun enimmäismäärä on saavutettu. Yritä uudelleen %1$d sekunnin kuluttua. - Lihavoitu otsikko Pariliitos epäonnistui. Myönnä lupa lähellä olevien laitteiden käyttöön ja yritä uudelleen. Paritus ei onnistunut. Yritä uudelleen. Lähistön laitteet -käyttöoikeus on poistettu käytöstä, joten radioosi ei voida muodostaa Bluetooth-yhteyttä. Napauta ottaaksesi käyttöoikeuden uudelleen käyttöön. Meshtastic ei voi muodostaa yhteyttä uudelleen Asetukset - Lähetyksen aikaväli Kanava varattu - Painikkeen GPIO-pinni - Summerin GPIO-pinni Tämän pienentäminen poistaa pysyvästi tallennetun historian %1$d laitteelta. Tämän pienentäminen poistaa pysyvästi tallennetun historian %1$d laitteelta. @@ -201,7 +182,6 @@ Esiasetettu viesti käytössä Kanavaa ei voitu vaihtaa, koska radiota ei ole vielä yhdistetty. Yritä uudelleen. Sammutusta ei tueta tällä laitteella - Karusellin aikaväli Nykyisen kanavan lähetyksen (TX) ja vastaanoton (RX) käyttöaste ja virheelliset lähetykset, eli häiriöt. Kanava @@ -215,7 +195,6 @@ Kanava 8 Kanavan ominaisuudet Kanavan URL-osoite on virheellinen, eikä sitä voida käyttää - Kanava Kanavan nimi Kanavan URL-osoite Kanavan käyttöaste @@ -260,9 +239,6 @@ CO₂ CO₂ kosteus CO₂ lämpötila - CODEC 2 käytössä - CODEC2 näytteenottotaajuus - Koodausnopeus Pienennä kaavio Suljettu Viestitä ilman verkkoyhteyttä ystäviesi ja yhteisösi kanssa ilman matkapuhelinverkkoa. @@ -275,49 +251,23 @@ Etäisyyden ja suunnan näyttäminen edellyttää sijaintilupaa. Tässä laitteessa ei ole kompassianturia. Suunta ei ole käytettävissä. Kompassin pohjoinen ylhäällä - Kompassin suuntaus Kompassi Arvioitu alue: \u00b1%1$s (\u00b1%2$s) Arvioitu alue: tarkkuus tuntematon Turvallisuusriski havaittu: avaimet ovat vaarantuneet. Valitse OK luodaksesi uudet. - Käsittele tuetun kiihtyvyysanturin kaksoisnapautusta käyttäjäpainikkeella. Hallitsee laitteen vilkkuvaa LED-valoa. Useimmissa laitteissa tällä voidaan ohjata yhtä neljästä LED-valosta, mutta latauksen tai GPS:n valoja ei voi hallita. - Lähetetäänkö naapuritiedot LoRa:n kautta sen lisäksi, että ne lähetetään MQTT-protokollalla ja PhoneAPI sovellusrajapinnassa? Tämä ei ole tuettu kanavalla, joka käyttää oletussalausavainta ja nimeä. Lähetä sijainti ensisijaisella kanavalla, kun käyttäjäpainiketta painetaan kolme kertaa. Aikavyöhyke laitteen näytöllä ja lokissa käytettäville päivämäärille. Käytä puhelimen aikavyöhykettä - Vaihtaa automaattisesti seuraavalle näytön sivulle kuin karuselli, määritetyn aikavälin perusteella. - Näytön ympyrän ulkopuolella oleva kompassisuunta osoittaa aina pohjoiseen. - Ohita oletusnäytön asettelu. - Käännä näyttö pystysuunnassa. - Tee näytön otsikkotekstistä lihavoitu. - Ohita OLED-näytön automaattinen tunnistus. - Kuinka kauan näyttö pysyy päällä sen jälkeen, kun käyttäjäpainiketta on painettu tai viestejä on vastaanotettu. - Laitteen näytöllä näytettävät yksiköt. - Edellyttää, että laitteessasi on kiihtyvyysanturi. - Laitteesi käyttämä taajuus lasketaan alueen, modeemin esiasetuksen ja tämän kentän perusteella. Kun arvo on 0, aikaväli lasketaan automaattisesti ensisijaisen kanavan nimen perusteella ja se poikkeaa oletus-julkisesta aikavälistä. Vaihda takaisin julkiseen oletus-aikaväliin, jos käytössä on yksityinen ensisijainen ja julkinen toissijainen kanava. - Asettaa maksimihyppyjen määrän, oletus on 3. Hyppyjen määrän lisääminen kasvattaa myös ruuhkaa, joten sitä tulisi käyttää varoen. 0 hypyn lähettämät viestit eivät saa kuittauksia (ACK). Tämän alueen esiasetukset on tarkoitettu vain lisensoiduille (radioamatööri) käyttäjille. Ota käyttöön **Licensed amateur radio (Ham)** käyttäjäasetuksissa -kohdassa, jotta voit valita ne. Saatavilla olevat modeemiesiasetukset, oletus on Long Fast. Alue, jossa aiot käyttää radiolaitteitasi. - Ethernetin ottaminen käyttöön poistaa Bluetooth-yhteyden sovellukseen. TCP-laiteyhteydet eivät ole käytettävissä Applen laitteilla. Ota käyttöön pakettien lähettäminen UDP:n kautta paikallisverkossa. - WiFi-yhteyden käyttöönotto katkaisee Bluetooth-yhteyden sovellukseen. - Suurin aikaväli, jonka aikana laite ei lähetä sijaintia. - Vähimmäisetäisyys metreinä, jonka muutos otetaan huomioon älykkäässä sijainnin lähetyksessä. - Nopein mahdollinen sijaintipäivitysten lähetysväli, kun minimietäisyys on täyttynyt. - Valinnaiset kentät, jotka sisällytetään sijaintiviesteihin. Mitä enemmän kenttiä sisällytetään, sitä suurempi viesti on, mikä pidentää lähetysaikaa ja lisää pakettihäviön riskiä. - Kuinka usein yritetään hakea GPS-sijainti (<10 sekuntia pitää GPS:n päällä). - Asetus laittaa kaiken mahdollisen lepotilaan. Seuranta- ja anturiroolissa tämä sisältää myös LoRa-radion. Älä käytä tätä asetusta, jos haluat käyttää laitetta puhelinsovellusten kanssa tai laitetta ilman käyttäjäpainiketta. Radio käynnistyy uudelleen ja on hetken aikaa tavoittamattomissa. - Julkinen avain, jolla on oikeus lähettää hallintaviestejä tälle laitteelle. - Tulosta reaaliaikainen virheenkorjausloki sarjaportin kautta, ja tarkastele sekä vie Bluetoothin kautta laitteesta poistettuja sijaintitietoja sisältäviä lokitiedostoja. - Laite on verkon ylläpitäjän hallinnoima, eikä käyttäjä pääse muokkaamaan laitteen asetuksia. Käytetään jaetun avaimen luomiseen etälaitteen kanssa. Laite ei jaa yksityistä avaintaan etähallinnan kautta. Voit asettaa uuden avaimen, mutta aiemmin asetettua avainta ei voi lukea takaisin. Luotu yksityisestä avaimestasi ja lähetetty muille verkon laitteille, jotta ne voivat laskea yhteisen salaisen avaimen. - Sarjaporttikonsoli käyttöön Stream API:n kautta. Asetukset Määritä Bluetooth-oikeudet Määritä kriittiset hälytykset @@ -369,7 +319,6 @@ Sisältää suodattimen Oletussuodattimet Suodattimet - Vianetsintälokirajapinta käytössä Ei näytettäviä sovelluslokitietoja Päivitä Vie lokitiedot @@ -418,8 +367,6 @@ Tiedot Havaitsemisanturi Tunnistinsensorin asetukset - Tunnistinsensori käytössä - Tunnistuksen tyyppi Laite Laitteen asetukset @@ -433,17 +380,15 @@ Laitteen mittausloki %1$s %1$s: %2$s%% - Laitemittareiden päivitysväli %1$s: %2$s V Laite on lepotilassa Laitteen tallennustila & käyttöliittymä (vain luku) - Lähetä laitteen telemetriatiedot - Ota käyttöön / poista käytöstä laitteen telemetriamoduuli, joka lähettää mittaustietoja mesh-verkkoon. Nämä ovat nimellisiä (oletus) arvoja. Ruuhkautuneissa mesh-verkoissa lähetysväli pitenee automaattisesti verkossa olevien (online) solmujen määrän perusteella. Teema: %1$s, Kieli: %2$s Kastepiste Yksityisviesti Suoran viestin avain Yksityisviestit + Ei käytössä Hylkää Katkaise yhteys Ei yhdistetty @@ -520,22 +465,16 @@ Pysäytä skannaus Tekoälyanalyysi ei ole saatavilla %1$s jäljellä - %1$d yksilöllistä radiota Näytä kartta Vapaa levytila %1$d - Näyttö Laitteen Näyttö - Näyttötila - Kun asetus on käytössä, laite näyttää 12 tunnin ajan näytössä. - Näyttöyksiköt Etäisyys Etäisyyden suodattimet Suodata laitelista ja meshtastic kartta puhelimesi läheisyyden perusteella. Etäisyyden mittaukset Näytä etäisyys puhelimen ja muiden sijainnin jakavien Meshtastic laitteiden välillä. - DNS Tyhjennä haku järjestelmätekoäly, gemini, avustaja, toiminnot, automaatio, ääni @@ -576,6 +515,7 @@ MQTT Radion mittaustiedot Laitteet + Ilmoitukset Aloittaminen Asetukset — moduulit & ylläpito Asetukset — radio & käyttäjä @@ -588,15 +528,14 @@ Käyttöohjeet Valmis Älä näytä enää tälle laitteelle - Kaksoisnapautus painikkeena MQTT-vastaanotto käytössä Julkisesta internet-yhdyskäytävästä tulevat viestit välitetään paikalliseen mesh-verkkoon. Nollahyppysääntöjen vuoksi oletuksena MQTT-palvelimelta tuleva liikenne ei etene tätä laitetta pidemmälle. Lataa Lataa tämä alue Päällekkäinen julkinen avain havaittu + Dynaaminen Luo vaivattomasti yksityisiä meshtastic verkkoja turvalliseen ja luotettavaan viestintään kaukana asutuista paikoista. - Palautus päällä Muokkaa Muokkaa karttatiilien verkkolähteen asetuksia 8 tuntia @@ -613,27 +552,18 @@ Emojien lataaminen epäonnistui Emoji-haku ei tuottanut tuloksia Viimeksi käytetyt - Ota virransäästötila käyttöön Käytössä - - Salaus käytössä Julkinen avain ei täsmää Julkinen avain ei vastaa tallennettua avainta. Voit poistaa laitteen ja antaa sen vaihtaa avaimet uudelleen, mutta tämä saattaa viitata vakavampaan tietoturvaongelmaan. Ota yhteyttä käyttäjään toista luotettua kanavaa pitkin selvittääksesi, johtuuko avaimen vaihtuminen tehdasasetusten palautuksesta tai muusta tarkoituksellisesta toimenpiteestä. Julkisen avaimen salaus Ympäristöarvot - Ympäristö - Ympäristötietojen moduuli käytössä - Näytä ympäristötiedot näytöllä - Ympäristömittareiden päivitysväli - Käytä Fahrenheit yksikköä Virhe Duty Cyclen raja saavutettu. Viestien lähettäminen ei ole tällä hetkellä mahdollista. Yritä myöhemmin uudelleen. Vakaan yhteyden muodostaminen epäonnistui useista yrityksistä huolimatta. Valitse radio uudelleen yrittääksesi uudestaan. Yhdistä & ylläpidä Muodostetaan etäyhteyttä… Verkon asetukset - Ethernet käytössä Ethernet-verkon IP: Tapahtuman laiteohjelmisto käynnissä Käytä tapahtuman teemaa @@ -649,7 +579,6 @@ Vie TAK-datapaketti Ulkoiset ilmoitukset Ulkoisten ilmoituksien asetukset - Ulkoiset ilmoitukset käytössä Palauta tehdasasetukset Kohtalainen Meshtastic %1$s @@ -660,10 +589,6 @@ Saatavilla olevat tiedostot (%1$d): Lisää sana tai regex-sääntö - Poista suodatus käytöstä - Ota suodatus käyttöön - Ota suodatus käyttöön - Piilota suodatussanoja sisältävät viestit Piilota %1$d suodatettu Suodatus Suodatettu @@ -772,6 +697,7 @@ Tämä voi kestää hetken... Kohde: %1$s Laiteohjelmiston päivitys + %1$d% Tuntematon virhe Tuntematon laitemalli: %1$d Tuntematon etäjulkaisu @@ -793,9 +719,7 @@ Tyhjennä laite päivityksen aikana Tyhjentää laitteen flash-muistin kokonaan ja asentaa sen jälkeen valitun laiteohjelmiston alusta alkaen. Firmwaren versio: %1$s - Kiinteä PIN-koodi Kiinteä sijainti - Käännä näyttö Lisätietoja saat tietosuojakäytännöstämme. Lihavoitu @@ -806,13 +730,7 @@ Vapaa muisti Käytettävissä oleva järjestelmämuisti tavuina Taajuus - Taajuuspaikka - Käyttäjäystävällinen nimi Kaasuvastus - Yhdyskäytävä - Luo syötetapahtuma vastapäivään käännettäessä - Luo syötetapahtuma myötäpäivään käännettäessä - Luo syötetapahtuma painettaessa Generoi QR-koodi Aluerajaus @@ -837,32 +755,18 @@ Näin pääset alkuun GitHub-repositorio Hyvä - GPIO GPIO pinni - GPIO-pinni kiertovalitsinta varten A-portti - GPIO-pinni kiertovalitsinta varten B-portti - GPIO-pinni kiertovalitsimen painallusportille - GPIO-pinni valvontaa varten - GPS EN GPIO-pinni - GPS-tila (fyysinen laitteisto) - GPS vastaanoton GPIO-pinni - GPS lähetyksen GPIO-pinni Myönnä käyttöoikeus - Vihreä Valinnainen. Lisätään kutsumerkkisi perään, esimerkiksi OH2ABC//Tukiasema Laite Laitteen malli Suunta - Valvontasignaali Ohje & documentaatio Piilota taso Piilota salasana - Historian maksimimäärä - Historian aikamäärä Tässä näkymässä ei kuultu yhtään radiota Radiot hyppymäärän mukaan - Hyppyjen määrä Hyppyjä Isäntälaite Isäntälaitteen mittausarvot @@ -870,18 +774,12 @@ Hyväksyn. Olen lukenut ja ymmärtänyt yllä olevan. Annan suostumuksen laitetietojeni salaamattomaan lähettämiseen MQTT:n kautta Tiedän mitä olen tekemässä. - I2S-kello - I2S-datatulo - I2S-datalähtö - I2S-sanan valinta IAQ Sisäilman laatu (IAQ) on suhteellinen asteikko, jota voidaan mitata mm. Bosch BME680 anturilla ja sen arvoväli on 0–500. Kuvakkeiden merkitykset - Jätä huomiotta Lisää '%1$s' jätä huomiotta listalle? Laite käynnistyy uudelleen muutoksen tekemisen jälkeen. Ohita saapuvat - Ohita MQTT Poistetaanko '%1$s' jätä huomiotta listalta? Laite käynnistyy uudelleen muutoksen tekemisen jälkeen. Asetusten tuonti @@ -909,7 +807,6 @@ IP IP-osoite: Portti: - IPv4-tila JSON ulostulo käytössä %1$s %1$s +%2$d @@ -932,8 +829,6 @@ Kuuluvuusalueen arvio Verkkotaso Lue lisää - Ledin valvontasignaali - LED-tila Vanha järjestelmänvalvojan kanava %1$d kirjastot @@ -1033,13 +928,10 @@ Luksi Hallitse mukautettuja karttatasoja Hallitse Karttatasoja - Hallintatila Manuaalinen sijaintipyyntö vaaditaan Mesh-kartta - Välimuistin tallennustilan määrä: %1$d Mt\nVälimuistin käyttö: %2$d Mt Välimuistin Hallinta - %1$s MB Nykyinen välimuistin koko %1$d Laattaa Tyhjennä kartan laatat @@ -1065,11 +957,8 @@ Säätutka SQL-välimuistin tyhjennys epäonnistui, katso logcat saadaksesi lisätietoja SQL-välimuisti tyhjennetty %1$s: lle - Karttaraportointi Salli salaamattomien laitetietojen jakaminen MQTT:n kautta Ottamalla tämän ominaisuuden käyttöön hyväksyt ja annat suostumuksen siihen, että laitteesi reaaliaikainen sijaintitieto lähetetään MQTT-protokollan kautta ilman salausta. Näitä sijaintitietoja voidaan käyttää esimerkiksi reaaliaikaiseen karttaraportointiin, laitteen seurantaan ja muihin vastaaviin telemetriatoimintoihin. - Karttaraportoinnin aikaväli (sekuntia) - Laitteesi lähettää määräajoin salaamattoman karttaraporttipaketin määritettyyn MQTT-palvelimeen. Tämä paketti sisältää seuraavat tiedot: tunnisteen, pitkän ja lyhyen nimen, likimääräisen sijainnin, laitemallin, roolin, laiteohjelmiston version, LoRa-alueen, modeemiesiasetuksen ja ensisijaisen kanavan nimen. Valitse ladattava kartta-alue Aloita Lataus Karttatyylin valinta @@ -1098,7 +987,6 @@ Mesh-verkon kutsut Kuuntele verkkokutsuja Vastaanota lähellä olevien mesh-verkkojen lähettämiä verkkokutsuja - Verkkokutsuviesti Enintään %1$d tavua Kanavia ei ole käytettävissä Lähellä oleva mesh-verkko kutsuu sinut liittymään @@ -1188,9 +1076,6 @@ %1$s%2$d µg/m³ Minimi - Minimilähetys (sekuntia) - Älykäs etäisyys - Älykäs aikaväli Vähimmäisherätyksen kesto Esiasetukset Moduulin asetukset @@ -1200,7 +1085,6 @@ MQTT MQTT asetukset - MQTT käytössä MQTT: yhteys katkesi MQTT: yhteys hylättiin (tarkista tunnistetiedot) MQTT-välityspalvelin epäonnistui: %1$s @@ -1214,8 +1098,6 @@ Yhteyttä välityspalvelimeen ei saada (TCP) Aikakatkaistu %1$d ms jälkeen TLS-yhteyden muodostaminen epäonnistui: %1$s - MQTT-välityspalvelin tällä puhelimella - Tämä puhelin välittää MQTT-liikennettä yhdistetyn radion puolesta. Poista asetus käytöstä katkaistaksesi välityksen välittömästi muuttamatta radion MQTT-asetuksia. Hyödyllinen tilanteissa, joissa MQTT-liikenne kuormittaa yhteyttä liikaa. Ota asetus uudelleen käyttöön jatkaaksesi välitystä. Yhdistetty Yhdistetään… Ei yhdistetty @@ -1238,7 +1120,6 @@ Mykistetty %1$d päiväksi, %2$s tunniksi Mykistetty %1$s tunniksi Ei mykistetty - Hälytysaikakatkaisu (sekuntia) Nimi Nimi ei voi olla tyhjä. Siirry takaisin @@ -1248,7 +1129,6 @@ Meshtastic vaatii yhteensopivan laitteen. Tukijamme ja yhteistyökumppanimme tarjoavat käyttövalmiita laitteita. Tässä on muutamia suosituimmista vaihtoehdoista. Naapuritieto Naapuritietojen asetukset - Naapuritiedot käytössä Verkko Uusi kanavan URL-osoite vastaanotettu Uudet viestit alla @@ -1261,7 +1141,6 @@ Bluetooth-laitteita ei löytynyt Mukautettuja karttalähteitä ei löytynyt. Ei laitetta valittuna - Laitteita ei löytynyt Tiedostoja ei löytynyt. Tilastoja ei ole saatavilla Karttatasoja ei ole ladattu. @@ -1323,7 +1202,6 @@ Suosikkien kautta MQTT:n kautta Tyhjennä NodeDB-tietokanta - Laitteen tietojen lähetyksen aikaväli Laitteet Laitteet tässä sijainnissa @@ -1342,6 +1220,8 @@ Ei nyt Merkintä Viestit + + Verkko Ilmoitukset on poistettu käytöstä, eikä Android enää pyydä käyttöoikeutta. Ota se käyttöön sovelluksen asetuksissa, jotta saat tiedon uusista viesteistä ja ilmoituksista. Ilman ilmoituksia Meshtastic ei voi ilmoittaa uusista viesteistä, uusista radioista tai akun alhaisesta varaustasosta, kun sovellus on taustalla. Ilmoitusten avulla Meshtastic voi tavoittaa sinut, kun sovellus ei ole avoinna: uusista viesteistä, uusista löydetyistä radioista ja radion alhaisesta varaustasosta. Jos et myönnä käyttöoikeutta, mikään muu ei muutu. @@ -1353,14 +1233,10 @@ Ilmoitukset hälytyksen/äänen saapumisesta Ilmoitukset saapuneesta viestistä Nyt - NTP palvelin - Kirjausten määrä Offline-kartat Alueita ei ole vielä ladattu - MQTT päällä OK - OLED-tyyppi 24 tuntia 1 tunti @@ -1379,23 +1255,13 @@ Avaa Wi-Fi-asetukset Valinnat Aseta kompassi pohjoiseen - - Ulostulon äänimerkki (GPIO) - Ulostulon kesto (millisekuntia) - Ulostulon LED aktiivinen - Ulostulon LED (GPIO) - Ulostulon värinä (GPIO) Lisävalikko Korvaa konsolin sarjaportti - Ohita käyttöaste (Duty Cycle) - Taajuuden ohitus - PA tuuletin pois käytöstä Pakettien aitous Tasapainoinen – Suosi todennettuja paketteja Suositeltu. Hylkää allekirjoittamattomat tason alentamisyritykset radioilta, joiden tiedetään allekirjoittavan paketit. Yhteensopiva – Hyväksy allekirjoittamattomat paketit - Todenna paketit aina kun mahdollista, mutta hyväksy myös allekirjoittamaton liikenne parhaan yhteensopivuuden varmistamiseksi. Suojaustaso Tiukka – Vaadi pakettien todentamista Ota tiukka tila käyttöön @@ -1403,7 +1269,6 @@ Näytä ja käsittele vain kryptografisesti todennetut mesh-paketit. Vanhemmat radiot ja ylikokoiset paketit eivät välttämättä enää toimi. Otetaanko tiukka tila käyttöön? Tämä yhdistetty laite ei tue pakettien allekirjoitusten vahvistamista. - Paritustila Salasana PAX @@ -1416,7 +1281,6 @@ WiFi: %1$s PAX-laskuri PAX-laskurin asetukset - PAX-laskuri käytössä Sijainnin toistuva lähetys Skannaa kanava- ja yhteystietojen QR-koodit @@ -1458,6 +1322,7 @@ %1$d sekunti %1$d sekuntia + PM1.0 PM10 PM2.5 @@ -1465,16 +1330,11 @@ Sijainti Aseta nykyisestä puhelimen sijainnista Sijainti käytössä - Sijaintimerkinnät Sijainti Sijainti Paketti - Virta Virran asetukset Virranhallinnan arvot - Virrankulutuksen moduuli käytössä - Virrankulutuksen näyttö käytössä - Virtamittareiden päivitysväli Powered ppm Tarkka sijainti @@ -1496,13 +1356,10 @@ Teksti Ensisijainen Säännöllinen sijainti- ja telemetrialähetys - Yksityinen avain Projektin tiedot Jaa puhelimen sijaintitietoa mesh-verkkoon Palveluntarjoajan nimi on olemassa. - Välityspalvelin käytössä PSK - PTT-pinni Julkinen avain Julkinen avain vaihdettu QR-koodi @@ -1520,25 +1377,14 @@ Sademäärä (24 h) Kuuluvuustesti Kuuluvuustestin asetukset - Kuuluvuustesti käytössä Reagoi Käynnistä uudelleen - - Uudelleenlähetyksen tila - Uudelleenlähettää kaikki havaitut viestit, jos ne ovat olleet omalla yksityisellä kanavalla tai toisessa mesh-verkosta, jossa on samat LoRa-parametrit. - Käyttäytyy samalla tavalla kuin ALL, mutta jättää pakettien purkamisen väliin ja lähettää niitä vain uudelleen. Mahdollista käyttää vain Repeater-roolissa. Tämän asettaminen muille rooleille johtaa ALL-käyttäytymiseen. - Ei ota huomioon paketteja, jotka tulevat ei-standardeista porttinumeroista, kuten: TAK, RangeTest, PaxCounter jne. Lähettää uudelleen vain paketteja, jotka käyttävät standardeja porttinumeroita: NodeInfo, Text, Position, Telemetry ja Routing. - Ei ota huomioon havaittuja viestejä ulkomaisista verkoista kuten LOCAL ONLY, mutta menee askeleen pidemmälle myös jättämällä huomiotta viestit laitteista, joita ei ole jo laitteen tuntemassa listassa. - Ei ota huomioon havaittuja viestejä ulkomaisista verkoista, jotka ovat avoimia tai joita se ei voi purkaa. Lähettää uudelleen viestin vain laitteen paikallisilla ensisijaisilla / toissijaisilla kanavilla. - Sallittu vain SENSOR-, TRACKER- ja TAK_TRACKER -rooleille. Tämä estää kaikki uudelleenlähetykset, toisin kuin CLIENT_MUTE -roolissa. Äskettäin havaitut verkkolaitteet Yhdistetään uudelleen… - Punainen Päivitä Päivitä metatiedot Haluatko varmasti luoda yksityisen avaimen uudelleen?\n\nLaitteet, jotka ovat aiemmin vaihtaneet avaimia tämän laitteen kanssa, joutuvat poistamaan kyseisen laitteen ja vaihtamaan avaimet uudelleen, jotta suojattu viestintä voi jatkua. Luo uusi yksityinen avain - Alue Kuultu %1$d radion kautta Kuultu %1$d radion kautta @@ -1589,32 +1435,17 @@ Laitteen rooli Client Client Base - Suosikkiradioihin liittyvät paketit käsitellään ROUTER_LATE-tilassa, muut paketit CLIENT-tilassa. - Yhdistetty sovellukseen tai itsenäinen viestintälaite. Client Hidden - Laite, joka lähettää vain tarvittaessa tai virransäästotilassa. Client Mute - Laite, joka ei välitä paketteja muilta laitteilta. Lost and Found - Lähettää sijainnin säännöllisesti oletuskanavalle helpottaakseen laitteen löytämistä. Repeater - Laite, joka laajentaa verkon kattavuutta välittämällä viestejä verkkoa kuormittamatta. Ei näy laitelistauksessa. Router Router Client - Yhdistelmä ROUTER sekä CLIENT roolista. Ei mobiililaitteille. - Laite, joka laajentaa verkon infrastruktuuria viestejä välittämällä. Näkyy laitelistauksessa. Router Late - Muuten samanlainen kuin ROUTER rooli, mutta se uudelleen lähettää paketteja vasta kaikkien muiden tilojen jälkeen, varmistaen paremman peittoalueen muille laitteille. Laite näkyy mesh-verkon laiteluettelossa muille käyttäjille. Sensor - Lähettää telemetriatiedot ensisijaisesti. TAK - Optimoitu ATAK-järjestelmän viestintään, joka vähentää tavanomaisia lähetyksiä. TAK Tracker - Ottaa käyttöön automaattisen TAK PLI -lähetyksen vähentäen tavanomaisia lähetyksiä. Tracker - Lähettää GPS-sijaintitiedot ensisijaisesti. - Palvelimen osoite (root topic) - Kiertovalitsin #1 käytössä Olen lukenut <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">dokumentaation</a> sekä blogikirjoituksen <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choosing The Right Device Role</a>. Ylläpitoistunto on vanhentunut @@ -1637,13 +1468,10 @@ Viesti on liian suuri lähetettäväksi RSSI Vastaanotetun signaalin voimakkuusindikaattori (RSSI) on mittari, jota käytetään määrittämään antennilla vastaanotetun signaalin voimakkuus. Korkeampi RSSI-arvo yleensä osoittaa vahvemman ja vakaamman yhteyden. - rsyslog-palvelin Satelliitit - Tallenna Tallenna & käynnistä uudelleen Tallenna - Tallenna .CSV (ESP32 ainoastaan) Vie kuuluvuustestin paketit Etsi @@ -1657,7 +1485,6 @@ Skannaa jaetun yhteystiedon QR-koodi Etsitään… Etsitään… - Näytön päälläoloaika Siirry loppuun Etsi emoji… Hae viesteistä… @@ -1690,19 +1517,8 @@ Valittu Valittu karttatyyppi Lähetä - Lähetä äänimerkki - Lähetä äänimerkki hälytyssanoman kanssa - Viestien lähetyksen aikaväli (sekuntia) - Sarjaliitäntä - Sarjaportin nopeus Sarjaportin asetukset - Sarjaporttikonsoli - Sarjaportti käytössä - Sarjaportin tila - RX - TX - Palvelin Istunto aktiivinen Päivitys vaaditaan Aseta aika @@ -1733,7 +1549,6 @@ Näytä reittipisteet Sammuta Laite: %1$s - Sammuta virran katketessa ⚠️ Tämä SAMMUTTAA laitteen. Saat laitteen takaisin toimintaan kytkemällä virran päälle. Signaali Signaalin laatu @@ -1767,7 +1582,6 @@ WebView järjestelmä päivittyy. Yritä myöhemmin uudelleen. Ohita Paikka - Älykäs sijainti SNR Signaali-kohinasuhde (SNR) on mittari, jota käytetään viestinnässä halutun signaalin tason ja taustahälyn tason määrittämisessä. Meshtasticissa ja muissa langattomissa järjestelmissä korkeampi SNR tarkoittaa selkeämpää signaalia, joka voi parantaa tiedonsiirron luotettavuutta ja laatua. Maaperän kosteus @@ -1775,17 +1589,12 @@ Nopeus %1$d Km/h %1$d mph - Levennyskerroin (Spread Factor) - SSID - Tilatiedon lähetys (sekuntia) Tilaviesti Julkinen tilaviesti, joka lähetetään mesh-verkkoon aina muuttuessaan ja lisäksi 12 tunnin välein. Pysy yhteydessä kaikkialla Lopeta yhdistäminen Varastoi & välitä Varastoi & välitä asetukset - Varastoi & välitä käytössä - Aliverkko Valmis Super-syväunen kesto Tuettu @@ -1793,21 +1602,10 @@ Poista Mykistä Poista mykistys - RX tehostettu vahvistus Järjestelmäasetukset TAK (ATAK) TAK-asetukset - Jäsenen rooli - Havaitsija etulinjassa - Päämaja - Koiraseuranta (K9) - Lääkäri - Radiopuhelinoperaattori - Tarkka-ampuja - Joukkueen johtaja - Tiimin jäsen - Määrittelemätön TAK palvelin TAK-mesh-kanava Lähtevään TAK-liikenteeseen käytettävä Meshtastic-kanava @@ -1842,22 +1640,6 @@ Suorita Käynnissä: %1$s Yhdistetyn radion laiteohjelmisto ei tue täydellistä TAK-integraatiota – vain sijaintitiedot ja chat-viestit välitetään ATAKiin. Merkit ja muut tapahtumatyypit edellyttävät laiteohjelmiston versiota 2.8.0 tai uudempaa. - Tiimin väri - Sininen - Ruskea - Turkoosi - Tummansininen - Tummanvihreä - Vihreä - Purppura - Viininpunainen - Oranssi - Liila - Punainen - Sinivihreä - Määrittelemätön - Valkoinen - Keltainen Telemetria Ympäristön asetukset Lämpötila @@ -1866,10 +1648,8 @@ Vaalea Järjestelmän oletus Aika - Aikavyöhyke Aikakatkaisu Aikaleima - TLS käytössä Kytke sijainti päälle Reitinselvitys @@ -1921,7 +1701,6 @@ Viesti on jo omalla kielelläsi Lähetys on poistettu käytöstä Tämä laite voi vastaanottaa, mutta ei lähetä mitään LoRa-verkon kautta. - Lähetä LoRa:n kautta Kuljetus API @@ -1935,12 +1714,12 @@ 24t 48 tuntia 2vko - Lähetys käytössä - Lähetysteho Kirjoita Kirjoita viesti UDP-lähetys Kumoa + dBm + m Yksiköt Järjestelmän oletus @@ -1961,9 +1740,6 @@ Poista kiinnitys Tuntematon Ei asetettu – 0 - Ylös/Alas/Valitse syöte käytössä - GPS-kyselyn aikaväli - Päivityksen aikaväli (sekuntia) Päivitetty MQTT-lähetys käytössä Verkosta tulevat viestit lähetetään julkiseen internetiin minkä tahansa laitteen määritetyn yhdyskäytävän kautta. @@ -1976,13 +1752,7 @@ URL-mallipohja USB USB-käyttöoikeus evättiin. Kytke laite uudelleen ja yritä uudestaan. - - Käytä 12 tunnin kelloa Kyrillisten merkkien tiivis koodaus - Käytä I2S protokollaa äänimerkille - Käytä INPUT_PULLUP tilaa - Käytä esiasetusta - Käytä PWM-äänimerkkiä Käyttäjä Käyttäjäasetukset @@ -1990,7 +1760,6 @@ Käyttäjätiedot Käyttäjän syöte Käyttäjätiedot - Käyttäjänimi UV-valon voimakkuus API-yhteyden kautta MQTT:n kautta @@ -1998,8 +1767,6 @@ Näytä kartalla Näytä versio Jännite - Bluetoothin odotusaika - Herätä napautuksesta tai liikkeestä Varoitus Poista reittipiste? Muokkaa reittipistettä @@ -2012,7 +1779,6 @@ WiFi-asetukset WiFi-määritys mPWRD-OS:lle - WiFi käytössä WiFI-verkon IP-osoite: Saatavilla olevat verkot Yhteyden muodostaminen epäonnistui: %1$s @@ -2048,7 +1814,6 @@ WiFi-määritys mPWRD-OS:lle Virheellinen WiFi-tunnusten QR-koodin muoto Skannaa WiFi:n QR-koodi - WiFi-signaalin RSSI-kynnys (oletus: -80) Et ole yhteydessä Wi-Fi-verkkoon. Verkkohaku ei välttämättä löydä lähellä olevia laitteita. Tuuli diff --git a/core/resources/src/commonMain/composeResources/values-fr/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-fr/schema_strings.xml new file mode 100644 index 0000000000..929fa30a69 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-fr/schema_strings.xml @@ -0,0 +1,295 @@ + + + + + Bleu + Actif + Vert + État de la LED + Rouge + Par défaut + Taux d'échantillonnage CODEC2 + CODEC 2 activé + Données de sortie I2S + Horloge I2C + Données d'entrée I2S + Selection de mot I2S + Broche PTT + Bluetooth activé + Code PIN fixe + Mode d'appariement + Code PIN fixe + Sans code PIN (connexion directe) + Générer un événement d'entrée sur CCW + Générer un événement d'entrée sur CW + Générer un événement d'entrée sur Appui + Broche GPIO pour un encodeur rotatif port A + Broche GPIO pour un encodeur rotatif port B + Broche GPIO pour un encodeur rotatif port Appui + Retour + Annuler + Bas + Gauche + Aucun + Droite + Sélectionner + Haut + Encodeur rotatif #1 activé + Envoyer une sonnerie + Entrée Haut/Bas/Select activée + Type du déclencheur de détection + Capteur de détection activé + Broche GPIO à surveiller + Nom convivial + Envoyer une sonnerie avec un message d'alerte + Haut + Utiliser le mode INPUT_PULLUP + GPIO du bouton + GPIO du buzzer + Désactiver le triple clic + Double clic comme appui sur le bouton + Traiter un double appui sur les accéléromètres compatibles comme une pression de bouton utilisateur. + LED de vérification de fonctionnement (heartbeat) + Intervalle de diffusion des infos nœud + Mode de réémission + Tout + Tout, saute le décodage + Identique au comportement de TOUS mais ignore le décodage des paquets et les rediffuse simplement. Uniquement disponible pour le rôle Répéteur. Définir cela sur tout autre rôle entraînera le comportement de TOUS. + Seulement les ports noyau + Connus seulement + Local uniquement + Ignore les messages observés à partir de maillages étrangers qui sont ouverts ou ceux qu'il ne peut pas déchiffrer. Ne diffuse que le message sur les nœuds des canaux primaires / secondaires. + Aucun + Seulement autorisé pour les rôles SENSOR, TRACKER et TAK_TRACKER, cela empêchera toutes les rediffusions, contrairement au rôle CLIENT_MUTE. + Rôle de l'appareil + Client + Base Client + Application connectée ou appareil de messagerie autonome. + Client masqué + Appareil ne diffusant que si nécessaire pour la discrétion et l'économie d'énergie. + Client muet + Appareil ne transmettant pas les paquets provenant d'autres appareils. + Objets trouvés + Transmet régulièrement la position par message dans le canal par défaut pour vous aider à retrouver l'appareil. + Répéteur + Routeur + Routeur Client + Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds. + Routeur avec retard + Capteur + Transmet les paquets de télémétrie en priorité. + TAK + Optimisé pour le système de communication ATAK, diminue les émissions de routine. + Traqueur TAK + Active les diffusions automatiques de TAK PLI et réduit les diffusions de routine. + Traqueur + Transmet les paquets de positions GPS en priorité. + Fuseau horaire + Intervalle du carrousel + Bascule automatiquement sur la page suivante de l'écran comme un carrousel, en fonction de l'intervalle spécifié. + Toujours pointer vers le nord + La direction de la boussole sur l'écran en dehors du cercle pointera toujours vers le nord. + Orientation de la boussole + Mode d'affichage + Remplacer la disposition par défaut de l'écran. + Métrique + Inverser l'écran + Retourner l’écran verticalement. + Titre en gras + Mettre en gras le texte de titre à l'écran. + Type d'OLED + Remplacer la détection automatique de l'écran OLED. + Écran allumé pour + Combien de temps l'écran reste allumé après que le bouton utilisateur soit appuyé ou que les messages soient reçus. + Unités d'affichage + Unités affichées sur l'écran de l'appareil. + Utiliser le format horaire 12h + Affiche l’heure au format 12 h une fois activé. + Réveil par appui ou mouvement + Nécessite un accéléromètre sur votre appareil. + Sortie LED active à l’état haut + LED à réception de la cloche d'alerte + Son à réception de la cloche d'alerte + Vibration à réception de la cloche d'alerte + LED à réception de message + Son à réception de message + Vibration à réception de message + Notifications externes activées + Délai d'expiration du message + LED extérieure (GPIO) + Buzzer extérieur (GPIO) + Sortie vibreur (GPIO) + Utiliser l'I2S comme buzzer + Utiliser le buzzer PWM + Bande Passante + Slot de fréquence + La fréquence de fonctionnement de votre nœud est calculée en fonction de la région, du préréglage du modem et de ce champ. Lorsque la valeur vaut 0, le slot est automatiquement calculé en fonction du nom du canal principal et sera différent de l'emplacement public par défaut. Revient à l'emplacement public par défaut si les canaux privés primaires et publics secondaires sont configurés. + Taux de codage + Transmission des paquets vers MQTT + Nombre de sauts + Définit le nombre maximum de sauts, la valeur par défaut est 3. L'augmentation des sauts augmente également la congestion et devrait être utilisée avec prudence. Les messages broadcast à 0 saut ne recevront pas les ACKs (confirmation de réception). + Ignorer MQTT + Préréglages + Longue portée-Rapide + Longue portée - Modérée + Longue portée - Lent + Longue portée - Turbo + Portée moyenne - Rapide + Portée moyenne - Lent + Portée courte - Rapide + Portée courte - Lent + Portée courte - Turbo + Très longue portée - Lent + Autoriser le dépassement du temps d'émission autorisé par heure + Remplacer la fréquence + Ventilateur PA désactivé + Région + La région où vous allez utiliser vos radios. + Facteur de propagation + Gain RX Boosté + Transmission activée + Puissance d'émission + Utiliser un préréglage + Observateur de transfert + Medic + Tireur d'élite + Chef d'équipe + Membre de l'équipe + Message + Adresse + MQTT activé + Chiffrement activé + Votre nœud enverra périodiquement un paquet de rapport de position non chiffré au serveur MQTT configuré. Ce paquet inclut l'identifiant, les noms long et court, la position approximative, le modèle matériel, le rôle, la version du micrologiciel, la région LoRa, le préréglage du modem et le nom du canal principal. + Mot de passe + Proxy pour le client activé + Sujet principal + TLS activé + Nom d'utilisateur + Infos de voisinage activées + Transmettre par LoRa + Que ce soit en plus de l'envoyer à MQTT et à PhoneAPI, notre NeighborInfo devrait être transmis par LoRa. Non disponible sur un canal avec la clé et le nom par défaut. + Fréquence de récupération GPS + Mode IPv4 + Activer les paquets de diffusion via UDP sur le réseau local. + Ethernet activé + Activer Ethernet désactivera la connexion Bluetooth à l'application. Les connexions de nœuds via TCP ne sont pas disponibles sur les appareils Apple. + DNS + Passerelle + IP + Sous-réseau + Serveur NTP + Aucun + Diffusion UDP + Serveur Rsyslog + Activer le Wi-Fi désactivera la connexion Bluetooth à l'application. + Mot de passe + SSID + Seuil BLE RSSI (par défaut -80) + Paxcounter activé + Fréquence de récupération GPS + Distance intelligente + Distance minimale en mètres pour considérer une diffusion de position intelligente. + Intervalle intelligent + Position fixe + GPIO EN du GPS + Mode GPS (matériel physique) + Fréquence de récupération GPS + À quelle fréquence devrions-nous essayer d'obtenir une position GPS (<10sec le GPS est maintenu allumé). + Désactivé + Activé + Absent + Intervalle de diffusion + L'intervalle maximum qui peut s'écouler sans qu'un nœud diffuse une position. + Position Intelligente + Champs de position + Champs optionnels à inclure dans les messages de position. Plus il y en a, plus le message est grand, plus cela augmentant le temps d'occupation du réseau et le risque de perte. + Altitude + Altitude au-dessus du niveau moyen de la mer + Séparation géoïdale de l’altitude + Cap du véhicule + Nombre de satellites + Numéro de séquence + Vitesse du véhicule + Horodatage + GPIO réception du GPS + GPIO émission du GPS + Facteur de remplacement du multiplicateur ADC + Activer le mode économie d'énergie + Sera en veille profonde autant que possible, pour les rôles traceurs et capteur, cela inclura également la radio LoRa. N'utilisez pas ce paramètre si vous voulez utiliser votre appareil avec les applications de téléphone ou si vous utilisez un appareil sans bouton utilisateur. + Arrêt en cas de perte d'alimentation + Durée d'attente max du Bluetooth + Test de portée activé + Enregistrer .CSV dans le stockage (ESP32 seulement) + Clé Admin + Clé publique autorisée à envoyer des messages d’administration à ce nœud. + API de journalisation de débogage activée + Afficher en direct les journaux de débogage via le port série, consulter/exporter les journaux sans position via Bluetooth. + Mode géré + L'appareil est géré par un administrateur de maillage, l'utilisateur ne peut accéder à aucun des paramètres de l'appareil. + Clé privée + Utilisée pour créer une clé partagée avec un appareil distant + Clé publique + Console série + Console série via l’API de flux. + Vitesse de transmission série + Vitesse de transmission série + Écho activé + Série activée + Mode série + RX + Par défaut + Par défaut + Positions NMEA + Message texte + Délai d'expiration + Tx + Stocker & Transférer activé + Battement de cœur (heartbeat) + Limite d’historique renvoyé + Fenêtre de retour d’historique + Serveur + Nombre d'enregistrements + Rôle + Bleu + Marron + Cyan + Bleu foncé + Vert Foncé + Vert + Magenta + Marron + Orange + Pourpre + Rouge + Turquoise + Blanc + Jaune + Module de mesure de la qualité de l'air activé + Intervalle de mise à jour des mesures de qualité d'air + Envoyer la télémétrie de l'appareil + Activez/désactivez le module de télémétrie de l'appareil pour envoyer des mesures (niveau de batterie, qualité du signal...) au maillage. Les maillages encombrés allongeront automatiquement l'intervalle en fonction du nombre de nœuds en ligne. + Intervalle de mise à jour des mesures + Les mesures environnementales utilisent Fahrenheit + Module de métriques de l'environnement activé + Mesures d'environnement à l'écran activées + Intervalle de mise à jour des mesures d'environnement + Module de mesure de puissance activé + Indicateurs d'alimentation à l'écran activés + Intervalle de mise à jour des mesures d'alimentation + Seuil de paquets inconnu + diff --git a/core/resources/src/commonMain/composeResources/values-fr/strings.xml b/core/resources/src/commonMain/composeResources/values-fr/strings.xml index 4433aba117..536f26fad7 100644 --- a/core/resources/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fr/strings.xml @@ -17,18 +17,19 @@ --> + Humidité %1$s: %2$s Message de %1$s: %2$s batterie : %1$d% À %1$s favori - À %1$d sauts dernier entendu %1$s hors-ligne en ligne rôle %1$s signal %1$s Saut %1$d : %2$d nœuds + Température A propros Accepter Remerciements @@ -45,7 +46,6 @@ Traduire le message Actions Remplacer le multiplicateur ADC - Facteur de remplacement du multiplicateur ADC Ajouter Ajouter une note privée… @@ -58,7 +58,6 @@ Ajouter un appareil manuellement… Ajouter une couche de réseau Adresse - Clé Admin Clés admin Administration Avancé @@ -68,24 +67,14 @@ Qualité de l'air Icône de la qualité de l'air Métriques de qualité de l'air - Module de mesure de la qualité de l'air activé - Intervalle de mise à jour des mesures de qualité d'air Pourcentage de temps d'antenne pour la transmission utilisée au cours de la dernière heure. UtilAir - - Son à réception de la cloche d'alerte - LED à réception de la cloche d'alerte Caractère d'appel ! - Vibration à réception de la cloche d'alerte - Son à réception de message - LED à réception de message - Vibration à réception de message Tout Autoriser la source d'entrée Autoriser l'accès non défini aux broches Alt Altitude - Toujours pointer vers le nord Lumière ambiante Configuration lumière ambiante Les statistiques sont collectées pour nous aider à améliorer l'application Android (merci), nous recevrons des informations anonymes sur le comportement de l'utilisateur. Cela inclut les rapports de plantage, les écrans utilisés dans l'application, etc. @@ -127,23 +116,18 @@ Sauvegardez les clés publiques et privées pour sécuriser le stockage chiffré sur cet appareil. Sauvegarder & Restaurer Mauvais - - Bande Passante Non supporté (%1$s) Cette bande passante n'est pas prise en charge par la radio connectée dans la région sélectionnée. Choisissez une valeur supportée avant d'enregistrer. Baro Batterie Adresse I2C de la batterie INA_2XX Appareils Bluetooth - Seuil BLE RSSI (par défaut -80) La recherche Bluetooth nécessite également des services de localisation activés pour cette version d'Android. Votre position n'est pas utilisée. - Bleu Bluetooth Périphériques Bluetooth disponibles Configuration Bluetooth Le Bluetooth est désactivé. Activez-le pour rechercher les appareils à proximité. - Bluetooth activé Configuration Gérer à distance sans fil les paramètres et les canaux de votre appareil. Découverte @@ -164,15 +148,11 @@ Limite d'analyse Bluetooth atteinte. Réessayez dans %1$d seconde. Limite d'analyse Bluetooth atteinte. Réessayez dans %1$d secondes. - Titre en gras L'appairage a échoué. Accorder les autorisations de l'appareil à proximité et essayer à nouveau. L'appairage n'a pas abouti. Veuillez réessayer. L'autorisation d'accès aux appareils à proximité est désactivée, votre radio ne peut donc pas être atteinte en Bluetooth. Appuyez pour l'allumer. Meshtastic ne peut pas se reconnecter Réglages - Intervalle de diffusion - GPIO du bouton - GPIO du buzzer Réduire ceci supprimera définitivement l'historique pour %1$d appareil. Réduire ceci supprimera définitivement l'historique pour %1$d appareils. @@ -190,7 +170,6 @@ Messages prédéfinis activés Impossible de modifier le canal, car la radio n'est pas encore connectée. Veuillez réessayer. Arrêt non pris en charge sur cet appareil - Intervalle du carrousel Utilisation pour le canal actuel, y compris TX bien formé, RX et RX mal formé (AKA bruit). Ch @@ -204,7 +183,6 @@ Canal 8 Fonctionnalités du canal Cette URL de canal est invalide et ne peut pas être utilisée - Canal Nom du canal URL du canal UtilCanal @@ -245,9 +223,6 @@ CO₂ CO₂ Humidité CO₂ Température - CODEC 2 activé - Taux d'échantillonnage CODEC2 - Taux de codage Réduire le graphique Réduit Communiquez en dehors du réseau avec vos amis et votre communauté sans service cellulaire. @@ -260,49 +235,23 @@ L'autorisation d'accès à la position est nécessaire pour afficher la distance et l'orientation. Cet appareil ne dispose pas de boussole. L'orientation n'est pas disponible. Nord de la boussole vers le haut - Orientation de la boussole Boussole Surface estimée : \u00b1%1$s (\u00b1%2$s) Surface estimée : précision inconnue Clés compromises détectées, sélectionnez OK pour régénérer. - Traiter un double appui sur les accéléromètres compatibles comme une pression de bouton utilisateur. Contrôle la LED clignotante sur l'appareil. Pour la plupart des appareils cela contrôlera une des 4 LED, celles du chargeur et du GPS ne sont pas contrôlables. - Que ce soit en plus de l'envoyer à MQTT et à PhoneAPI, notre NeighborInfo devrait être transmis par LoRa. Non disponible sur un canal avec la clé et le nom par défaut. Envoyer une position sur le canal principal lorsque le bouton utilisateur est triple-cliqué. Fuseau horaire pour les dates sur l'écran de l'appareil et les logs. Utiliser le fuseau horaire du téléphone - Bascule automatiquement sur la page suivante de l'écran comme un carrousel, en fonction de l'intervalle spécifié. - La direction de la boussole sur l'écran en dehors du cercle pointera toujours vers le nord. - Remplacer la disposition par défaut de l'écran. - Retourner l’écran verticalement. - Mettre en gras le texte de titre à l'écran. - Remplacer la détection automatique de l'écran OLED. - Combien de temps l'écran reste allumé après que le bouton utilisateur soit appuyé ou que les messages soient reçus. - Unités affichées sur l'écran de l'appareil. - Nécessite un accéléromètre sur votre appareil. - La fréquence de fonctionnement de votre nœud est calculée en fonction de la région, du préréglage du modem et de ce champ. Lorsque la valeur vaut 0, le slot est automatiquement calculé en fonction du nom du canal principal et sera différent de l'emplacement public par défaut. Revient à l'emplacement public par défaut si les canaux privés primaires et publics secondaires sont configurés. - Définit le nombre maximum de sauts, la valeur par défaut est 3. L'augmentation des sauts augmente également la congestion et devrait être utilisée avec prudence. Les messages broadcast à 0 saut ne recevront pas les ACKs (confirmation de réception). Les préréglages de cette région sont réservés aux opérateurs autorisés (radio amateur). Activez la radio amateur autorisée (Ham) dans la configuration utilisateur pour les sélectionner. Préréglages de modem disponibles, la valeur par défaut est Long Fast. La région où vous allez utiliser vos radios. - Activer Ethernet désactivera la connexion Bluetooth à l'application. Les connexions de nœuds via TCP ne sont pas disponibles sur les appareils Apple. Activer les paquets de diffusion via UDP sur le réseau local. - Activer le Wi-Fi désactivera la connexion Bluetooth à l'application. - L'intervalle maximum qui peut s'écouler sans qu'un nœud diffuse une position. - Distance minimale en mètres pour considérer une diffusion de position intelligente. - Intervalle minimum auquel les mises à jour de position seront envoyées si la distance minimale est respectée. - Champs optionnels à inclure dans les messages de position. Plus il y en a, plus le message est grand, plus cela augmentant le temps d'occupation du réseau et le risque de perte. - À quelle fréquence devrions-nous essayer d'obtenir une position GPS (<10sec le GPS est maintenu allumé). - Sera en veille profonde autant que possible, pour les rôles traceurs et capteur, cela inclura également la radio LoRa. N'utilisez pas ce paramètre si vous voulez utiliser votre appareil avec les applications de téléphone ou si vous utilisez un appareil sans bouton utilisateur. Le nœud est en cours de redémarrage et sera brièvement inaccessible. - Clé publique autorisée à envoyer des messages d’administration à ce nœud. - Afficher en direct les journaux de débogage via le port série, consulter/exporter les journaux sans position via Bluetooth. - L'appareil est géré par un administrateur de maillage, l'utilisateur ne peut accéder à aucun des paramètres de l'appareil. Utilisée pour créer une clé partagée avec un appareil distant. L'appareil ne partage pas sa clé privée sur l'administration à distance. Vous pouvez définir une nouvelle clé, mais elle ne pourra ne jamais être lue de nouveau. Généré à partir de votre clé publique et envoyé à d'autres nœuds sur le maillage pour leur permettre de calculer une clé secrète partagée. - Console série via l’API de flux. Configuration Configurer les autorisations Bluetooth Configurer les alertes critiques @@ -350,7 +299,6 @@ Filtre inclus Filtres prédéfinis Filtres - API de journalisation de débogage activée Aucun journal d'application à afficher Actualiser Exporter les logs @@ -399,8 +347,6 @@ Détails Capteur de détection Configuration du capteur de détection - Capteur de détection activé - Type du déclencheur de détection Appareil Configuration de l'appareil @@ -414,17 +360,15 @@ Métriques de l’appareil %1$s %1$s / %2$s%% - Intervalle de mise à jour des mesures %1$s: %2$s V Appareil en veille Stockage de l'appareil & UI (lecture seule) - Envoyer la télémétrie de l'appareil - Activez/désactivez le module de télémétrie de l'appareil pour envoyer des mesures (niveau de batterie, qualité du signal...) au maillage. Les maillages encombrés allongeront automatiquement l'intervalle en fonction du nombre de nœuds en ligne. Thème %1$s, Langue %2$s Point de rosée Message direct Clé de message direct Messages directs + Désactivé Ignorer Déconnecter Déconnecté @@ -493,19 +437,14 @@ %1$s restants Voir la carte Disque libre %1$d - Écran Affichage de l'appareil - Mode d'affichage - Affiche l’heure au format 12 h une fois activé. - Unités d'affichage Distance Filtres de distance Filtrer la liste de nœuds et la carte de maillage en fonction de la proximité de votre téléphone. Mesures de distance Afficher la distance entre votre téléphone et les autres nœuds Meshtastic avec des positions. - DNS Effacer la recherche Rechercher dans la documentation @@ -524,13 +463,12 @@ Documentation Terminé Ne plus afficher pour cet appareil - Double clic comme appui sur le bouton Les messages provenant d'une passerelle Internet publique sont transmis au maillage local. En raison de la politique zéro saut, le trafic du serveur MQTT par défaut ne se propagera pas plus loin que cet appareil. Télécharger Clé publique dupliquée détectée + Dynamique Installez facilement des réseaux de maillage privé pour une communication sûre et fiable dans les régions éloignées. - Écho activé Modifier Modifier le réseau de tuile personnalisée 8 Heures @@ -541,27 +479,18 @@ Impossible de charger les émojis Aucun émoji trouvé Récemment utilisé - Activer le mode économie d'énergie Activé - - Chiffrement activé Non-concordance de clé publique La clé publique ne correspond pas à la clé enregistrée. Vous pouvez supprimer le nœud et le laisser à nouveau échanger les clés, mais cela peut indiquer un problème de sécurité plus grave. Contactez l'utilisateur à travers un autre canal de confiance, pour déterminer si le changement de clé est dû à une réinitialisation d'usine ou à une autre action intentionnelle. Chiffrement de clé publique Métriques d'environnement - Environnement - Module de métriques de l'environnement activé - Mesures d'environnement à l'écran activées - Intervalle de mise à jour des mesures d'environnement - Les mesures environnementales utilisent Fahrenheit Erreur Limite du temps d'émission autorisé par heure (duty cycle) atteinte. Vous ne pouvez pas envoyer de messages maintenant, veuillez réessayer plus tard. Impossible d'établir une connexion stable après plusieurs tentatives. Veuillez sélectionner à nouveau le nœud pour réessayer. Connecter & administrer Établissement de la session distante… Options Ethernet - Ethernet activé IP Ethernet : Demander position Agrandir le graphique @@ -575,7 +504,6 @@ Exporter le paquet de données TAK Notification externe Configuration de notification externe - Notifications externes activées Réinitialisation d'usine Passable Meshtastic %1$s @@ -586,10 +514,6 @@ Fichiers disponibles (%1$d ) : Ajouter un mot ou une expression régulière : modèle - Désactiver le filtrage - Activer le filtrage - Activer le filtrage - Cacher les messages contenant les mots du filtre Masquer %1$d filtré Filtre Filtré @@ -677,6 +601,7 @@ Ça peut prendre une minute... Destination : %1$s Mise à jour du firmware + %1$d% Une erreur inconnue s'est produite Modèle de matériel inconnu : %1$d Version distante inconnue @@ -693,9 +618,7 @@ En attente du redémarrage de l'appareil en mode OTA... En attente de la reconnexion de l'appareil ... Version du firmware : %1$s - Code PIN fixe Position fixe - Inverser l'écran Pour plus d'informations, consultez notre politique de confidentialité. Gras @@ -706,13 +629,7 @@ Mémoire libre Mémoire système disponible en octets Fréq - Slot de fréquence - Nom convivial Résistance au gaz - Passerelle - Générer un événement d'entrée sur CCW - Générer un événement d'entrée sur CW - Générer un événement d'entrée sur Appui Générer un QR Code Zone géographique @@ -725,27 +642,13 @@ Commencer Dépôt GitHub Bon - GPIO Broche GPIO - Broche GPIO pour un encodeur rotatif port A - Broche GPIO pour un encodeur rotatif port B - Broche GPIO pour un encodeur rotatif port Appui - Broche GPIO à surveiller - GPIO EN du GPS - Mode GPS (matériel physique) - GPIO réception du GPS - GPIO émission du GPS - Vert Matériel Modèle de matériel En-tête - Battement de cœur (heartbeat) Ajouter un calque Masquer le mot de passe - Limite d’historique renvoyé - Fenêtre de retour d’historique - Nombre de sauts Sauts Hôte Métriques de l’hôte @@ -753,18 +656,12 @@ J'accepte. J’ai lu et compris ce qui précède. Je consens volontairement à la transmission non chiffrée des données de mon nœud via MQTT. Je sais ce que je fais. - Horloge I2C - Données d'entrée I2S - Données de sortie I2S - Selection de mot I2S IAQ (Qualité de l'air intérieur) valeur de l'échelle relative IAQ mesurée par Bosch BME680. Plage de valeur 0–500. Signification des icônes - Ignorer Ajouter '%1$s' à la liste des ignorés ? Votre radio va redémarrer après avoir effectué ce changement. Ignorer les entrées - Ignorer MQTT Supprimer '%1$s' de la liste des ignorés ? Votre radio va redémarrer après avoir effectué ce changement. Importer la configuration @@ -785,7 +682,6 @@ IP Adresse IP: Port : - Mode IPv4 Sortie JSON activée %1$s @@ -799,8 +695,6 @@ Latitude En savoir plus - LED de vérification de fonctionnement (heartbeat) - État de la LED Ancien canal Admin %1$d Bibliothèques @@ -885,11 +779,9 @@ Lux Gérer les sources de tuiles personnalisées Gérer les calques de la carte - Mode géré Requête manuelle de position requise Carte de maillage - Capacité du cache : %1$d MB\nUtilisation du cache : %2$d MB Gestionnaire du cache Taille actuelle du cache %1$d tuiles @@ -907,11 +799,8 @@ Gestionnaire hors-ligne La purge du cache SQL a échoué, consultez « logcat » pour plus de détails Cache SQL purgé pour %1$s - Rapport cartographique Consentir au partage des données non chiffrées du nœud via MQTT En activant cette fonctionnalité, vous reconnaissez et consentez expressément à la transmission de la position géographique en temps réel de votre appareil via le protocole MQTT, sans chiffrement. Ces données de localisation peuvent être utilisées à des fins telles que l’affichage sur une carte en temps réel, le suivi de l’appareil et d’autres fonctions de télémétrie associées. - Intervalle de rapport cartographique (secondes) - Votre nœud enverra périodiquement un paquet de rapport de position non chiffré au serveur MQTT configuré. Ce paquet inclut l'identifiant, les noms long et court, la position approximative, le modèle matériel, le rôle, la version du micrologiciel, la région LoRa, le préréglage du modem et le nom du canal principal. Sélectionnez la région de téléchargement Commencer le téléchargement Sélection du style de carte @@ -958,9 +847,6 @@ Inconnu Messages Min - Diffusion minimale (secondes) - Distance intelligente - Intervalle intelligent Durée minimale de réveil Préréglages Réglages du module @@ -970,7 +856,6 @@ MQTT Configuration MQTT - MQTT activé Hôte introuvable Échec de la connexion Broker rejeté : %1$s @@ -999,14 +884,12 @@ Muet pour %1$d jours, %2$s heures Muet pour %1$s heures Non muet - Durée de répétition de la sortie (secondes) Nom Le nom ne peut pas être vide. Précédent Naviguer vers Informations sur les voisins Configuration des informations du voisinage - Infos de voisinage activées Réseau Réception de l'URL d'un nouveau cana Nouveaux messages au-dessous @@ -1019,7 +902,6 @@ Aucun périphérique Bluetooth détecté Aucune source de tuiles personnalisées trouvée. Aucun appareil sélectionné - Aucun appareil trouvé Aucun fichier affiché. Pas de stats disponibles Aucun calque personnalisé chargé. @@ -1059,7 +941,6 @@ par Favoris via MQTT Reconfiguration de NodeDB - Intervalle de diffusion des infos nœud Nœuds Nœuds à cet emplacement @@ -1074,6 +955,8 @@ Non connecté Note Notes + + Maillage Meshtastic utilise les notifications pour vous tenir à jour sur les nouveaux messages et autres événements importants. Vous pouvez mettre à jour vos autorisations de notification à tout moment à partir des paramètres. Notifications pour le canal et les messages directs. @@ -1082,12 +965,8 @@ Notifications sur réception d'alerte/cloche Notifications à la réception d'un message Maintenant - Serveur NTP - Nombre d'enregistrements - Transmission des paquets vers MQTT D'accord - Type d'OLED 24 Heures 1 Heure @@ -1103,19 +982,9 @@ Bibliothèques Open Source Options Orienter vers le nord - - Buzzer extérieur (GPIO) - Durée de sortie (en millisecondes) - Sortie LED active à l’état haut - LED extérieure (GPIO) - Sortie vibreur (GPIO) Menu supplémentaire Outrepasser le port série de la console - Autoriser le dépassement du temps d'émission autorisé par heure - Remplacer la fréquence - Ventilateur PA désactivé - Mode d'appariement Mot de passe PAX @@ -1127,7 +996,6 @@ W :%1$d Compteur de passages Configuration du Paxcounter - Paxcounter activé Diffusion périodique de position Meshtastic a besoin des autorisations "Périphériques à proximité" activées pour trouver et se connecter à des appareils via Bluetooth. Vous pouvez désactiver la lorsque la localisation n'est pas utilisée. @@ -1148,20 +1016,16 @@ %1$d seconde %1$d secondes + Position Définir à partir de l'emplacement actuel du téléphone Position activée - Champs de position Position Paquet de position - Alimentation Configuration de l'alimentation Métriques d'alimentation - Module de mesure de puissance activé - Indicateurs d'alimentation à l'écran activés - Intervalle de mise à jour des mesures d'alimentation Alimenté Emplacement précis Langue @@ -1182,12 +1046,9 @@ Texte Principal Diffusion périodique de la position et des données de télémétrie - Clé privée Fournir l'emplacement au maillage Le nom du fournisseur existe déjà. - Proxy pour le client activé PSK (clé) - Broche PTT Clé publique Clé publique modifiée Code QR @@ -1205,25 +1066,14 @@ Pluie (24h) Tests de portée Configuration des tests de portée - Test de portée activé Réagir Redémarrer - - Mode de réémission - Rediffuser tout message observé, s'il était sur notre canal privé ou à partir d'un autre maillage avec les mêmes paramètres LoRa. - Identique au comportement de TOUS mais ignore le décodage des paquets et les rediffuse simplement. Uniquement disponible pour le rôle Répéteur. Définir cela sur tout autre rôle entraînera le comportement de TOUS. - Ignore les paquets de portnums non standards tels que : TAK, RangeTest, PaxCounter, etc. Retransmet seulement les paquets avec des portnums standard : NodeInfo, Text, Position, Télémétrie et Routing. - Ignore les messages observés depuis des maillages distants comme LOCAL SEULEMENT, mais va plus loin en ignorant également les messages des nœuds qui ne sont pas déjà dans la liste connue du nœud. - Ignore les messages observés à partir de maillages étrangers qui sont ouverts ou ceux qu'il ne peut pas déchiffrer. Ne diffuse que le message sur les nœuds des canaux primaires / secondaires. - Seulement autorisé pour les rôles SENSOR, TRACKER et TAK_TRACKER, cela empêchera toutes les rediffusions, contrairement au rôle CLIENT_MUTE. Périphériques réseaux récents Reconnexion… - Rouge Actualiser Actualiser les métadonnées Êtes-vous sûr de vouloir régénérer votre clé privée ?\n\nLes nœuds qui peuvent avoir précédemment échangé des clés avec ce nœud devront supprimer ce nœud et ré-échanger des clés afin de reprendre une communication sécurisée. Régénérer la clé privée - Région Entendu par %1$d relai Entendu par %1$d relais @@ -1272,31 +1122,17 @@ Rôle de l'appareil Client Base Client - Traite les paquets depuis ou vers les nœuds favoris comme Routeur avec retard (ROUTER_LATE), et tous les autres paquets comme CLIENT. - Dispositif de messagerie autonome ou connecté à l'application. Client masqué - Appareil ne diffusant que si nécessaire pour la discrétion et l'économie d'énergie. Client muet - Appareil ne transmettant pas les paquets provenant d'autres appareils. Objets trouvés Répéteur - Nœud d'infrastructure pour étendre la couverture réseau en relayant les messages avec une surcharge minimale. Non visible dans la liste des nœuds. Routeur Routeur Client - Combinaison à la fois du ROUTER et du CLIENT. Pas pour les appareils mobiles. - Nœud d'infrastructure pour étendre la couverture réseau en relayant les messages. Visible dans la liste des nœuds. Routeur avec retard - Nœud d'infrastructure qui retransmet toujours les paquets une fois mais seulement après tous les autres modes, assurant une couverture supplémentaire pour les clusters locaux. Visible dans la liste des nœuds. Capteur - Transmet les paquets de télémétrie en priorité. TAK - Optimisé pour le système de communication ATAK, diminue les émissions de routine. Traqueur TAK - Active les diffusions automatiques de TAK PLI et réduit les diffusions de routine. Traqueur - Transmet les paquets de positions GPS en priorité. - Sujet principal - Encodeur rotatif #1 activé J'ai lu la <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Documentation du rôle de l'appareil</a> et le billet de blog sur comment <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choisir le rôle de l'appareil approprié</a>. Accusé de réception négatif @@ -1305,12 +1141,9 @@ Délai dépassé RSSI Indicateur de force du signal reçu, une mesure utilisée pour déterminer le niveau de puissance reçu par l'antenne. Une valeur RSSI plus élevée indique généralement une connexion plus forte et plus stable. - Serveur Rsyslog Sats - Enregistrer Sauvegarder - Enregistrer .CSV dans le stockage (ESP32 seulement) Exporter les paquets tests de portée Scanner @@ -1322,7 +1155,6 @@ Scanner le code QR du contact partagé Recherche… Recherche… - Écran allumé pour Défiler vers le bas Rechercher des émojis... Secondaire @@ -1350,19 +1182,8 @@ Sélectionné Type de carte sélectionné Envoyer - Envoyer une sonnerie - Envoyer une sonnerie avec un message d'alerte - Intervalle de message de l'expéditeur (secondes) - Série - Vitesse de transmission série Configuration série - Console série - Série activée - Mode série - RX - Tx - Serveur Session active Actualisation requise Configurer la connexion @@ -1386,7 +1207,6 @@ Afficher les points de repère Éteindre Nœud : %1$s - Arrêt en cas de perte d'alimentation ⚠️ Vous allez ETEINDRE le nœud. Une interaction physique sera requise pour le rallumer. Signal Qualité du signal @@ -1394,62 +1214,29 @@ Écran Ignorer Emplacement - Position Intelligente SNR Signal-to-Noise Ratio, une mesure utilisée dans les communications pour quantifier le niveau du signal par rapport au niveau du bruit de fond. Dans les systèmes Meshtastic et autres systèmes sans fil, un SNR plus élevé indique un signal plus clair qui peut améliorer la fiabilité et la qualité de la transmission de données. Hum sol Temp sol Vitesse %1$d Km/h - Facteur de propagation - SSID - Diffusion de l'État (secondes) Statut du message Restez connecté n'importe où Stocker & Transférer Stocker & Transférer la configuration - Stocker & Transférer activé - Sous-réseau Durée du sommeil extra profond Pris en charge Soutenu par la communauté Meshtastic Effacer Mode Muet Désactiver Muet - Gain RX Boosté Paramètres système TAK (ATAK) Configuration TAK - Rôle Membre - Observateur de transfert - Quartier général - Doggo (K9) - Medic - Opérateur de radio téléphonie - Tireur d'élite - Chef d'équipe - Membre de l'équipe - Non spécifié Activer le serveur TAK local Serveur Éteint - Couleur de l'équipe - Bleu - Marron - Cyan - Bleu foncé - Vert Foncé - Vert - Magenta - Marron - Orange - Pourpre - Rouge - Turquoise - Non spécifié - Blanc - Jaune Télémétrie Configuration de la Télémétrie Temp @@ -1458,10 +1245,8 @@ Clair Valeur par défaut du système Heure - Fuseau horaire Délai d'expiration Horodatage - TLS activé Basculer ma position Tracer la route @@ -1504,7 +1289,6 @@ Conserver les sauts du Routeur Seuil de paquets inconnu - Transmettre par LoRa BLE LoRa @@ -1515,10 +1299,10 @@ 24H 48 Heures 2S - Transmission activée - Puissance d'émission Type Composer un message + dBm + m Valeur par défaut du système Métrique @@ -1534,9 +1318,6 @@ Désactiver Muet Non reconnu Non défini - 0 - Entrée Haut/Bas/Select activée - Fréquence de récupération GPS - Intervalle de mise à jour (secondes) Mis à jour Les messages provenant du maillage seront envoyés à Internet public via la passerelle configurée sur n'importe quel nœud. Durée de fonctionnement @@ -1546,13 +1327,7 @@ L'URL doit contenir des espaces réservés. Modèle d'URL USB - - Utiliser le format horaire 12h Encodage compact pour Cyrillique - Utiliser l'I2S comme buzzer - Utiliser le mode INPUT_PULLUP - Utiliser un préréglage - Utiliser le buzzer PWM Utilisateur Configuration de l'utilisateur @@ -1560,7 +1335,6 @@ Infos utilisateur Texte utilisateur Infos utilisateur - Nom d'utilisateur UV Lux via API via MQTT @@ -1568,8 +1342,6 @@ Afficher sur la carte Voir la version Tension - Durée d'attente max du Bluetooth - Réveil par appui ou mouvement Attention Supprimer le repère ? Modifier le repère diff --git a/core/resources/src/commonMain/composeResources/values-ga/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ga/schema_strings.xml new file mode 100644 index 0000000000..6c09c0d409 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ga/schema_strings.xml @@ -0,0 +1,38 @@ + + + + + Cealaigh + Ní dhéanfaidh sé + Ainm + Ceim misniúla thosaí go lucht shnaithte! + Ní dhéanfaidh sé + Ceadaítear é seo ach amháin do na róil SENSOR, TRACKER agus TAK_TRACKER, agus cuirfidh sé bac ar gach athdháileadh, cosúil leis an róil CLIENT_MUTE. + Feiste a seolann ach nuair is gá, chun éalú nó do chumas cothromóige. + Feiste nach dtarchuir pacáistí ó ghléasanna eile. + Pacáiste leis an suíomh agus é seolta chuig an cainéal réamhshocraithe gach lá. + Bíonn sé ag seoladh pacáistí teiliméadair mar thosaíocht. + Optamaithe le haghaidh cumarsáide ATAK, laghdaíonn sé cainéil seirbhíse beacht. + Ceadaíonn fadhb beathú do bhoganna PLI i sórtú PLI i feidhm reatha. + Bíonn sé ag seoladh pacáistí suíomh GPS mar thosaíocht. + Réigiún + Teachtaireacht + Ní dhéanfaidh sé + Sábháil + Am tráth + diff --git a/core/resources/src/commonMain/composeResources/values-ga/strings.xml b/core/resources/src/commonMain/composeResources/values-ga/strings.xml index 7acea22424..5044b31fa7 100644 --- a/core/resources/src/commonMain/composeResources/values-ga/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ga/strings.xml @@ -17,6 +17,8 @@ --> + Laige + Teocht Maidir le Glac @@ -27,7 +29,6 @@ Rialachas Céatadán de na hamaitear úsáideach atá in úsáid laistigh de uair an chloig atá caite. - Nuashonrú feidhmchláir riachtanach Cuir i bhfeidhm @@ -35,7 +36,6 @@ Maith An bhfuil tú cinnte gur mhaith leat an cainéal réamhshocraithe a athrú? Go dona - Cúis leictreachais Á ríomh… @@ -46,7 +46,6 @@ Úsáid na cainéil reatha, lena n-áirítear TX ceartaithe, RX agus RX mícheart (anáilís ar na fuaimeanna). Tá an URL Cainéil seo neamhdhleathach agus ní féidir é a úsáid - Cainéal Ainm Cainéal Roghnaigh téama @@ -80,17 +79,15 @@ Na ceangailte Direach - Sáth + Cuir in eagar 8 Uair an chloig - Mícomhoiriúnacht na heochrach phoiblí Cóid Poiblí Eochair - Earráid Teorainn na Ciorcad Oibre bainte. Ní féidir teachtaireachtaí a sheoladh faoi láthair, déan iarracht arís níos déanaí. @@ -105,11 +102,9 @@ Maith - Céimeanna uaidh QAÍ (Cáilíocht Aeir Inmheánach) (Cáilíocht Aeir Inmheánach) scála ábhartha den luach QAÍ a thomhas ag Bosch BME680. Scála Luach 0–500. - Ignóra Cuir ‘%1$s’ leis an liosta ignorálacha? Bain ‘%1$s’ ón liosta ignorálacha? @@ -130,7 +125,6 @@ Lógáil - Cumas an Cásla: %1$d MB\nÚsáid an Cásla: %2$d MB Bainisteoir Cásla Méid na Cásla Reatha %1$d tíleanna @@ -188,19 +182,19 @@ Ní aon (diúscairt) Ní dhéanfaidh sé Ní ceangailte + Ceadaigh 24 Uair an chloig - + - Teanga Réamhshocrú córas @@ -214,13 +208,6 @@ Comhrá tapa nua Cumraíocht raidió Athmhaoinigh - - Athsheoladh aon teachtaireacht i ndáiríre má bhí sí oiriúnach le do cheist go léannais foghlamhrúcháin. - Ceim misniúla thosaí go lucht shnaithte! - Cuireann sé bac ar phacáistí ó phortníomhaíochtaí neamhchaighdeánacha mar: TAK, RangeTest, PaxCounter, srl. Ní athdháileann ach pacáistí le portníomhaíochtaí caighdeánacha: NodeInfo, Text, Position, Telemetry, agus Routing. - Cuireann sé bac ar theachtaireachtaí a fhaightear ó mhóilíní seachtracha cosúil le LOCAL ONLY, ach téann sé céim níos faide trí theachtaireachtaí ó nóid nach bhfuil sa liosta aitheanta ag an nóid a chosc freisin. - Ceadaítear é seo ach amháin do na róil SENSOR, TRACKER agus TAK_TRACKER, agus cuirfidh sé bac ar gach athdháileadh, cosúil leis an róil CLIENT_MUTE. - Réigiún Rialú iargúlta @@ -233,23 +220,12 @@ Athshocraigh Athshocrú go dtí na réamhshocruithe - Feiste nascaithe nó feiste teachtaireachtaí standálaí. - Feiste a seolann ach nuair is gá, chun éalú nó do chumas cothromóige. - Feiste nach dtarchuir pacáistí ó ghléasanna eile. - Ceannaire infreastruchtúrtha chun clúdach líonra a leathnú trí theachtaireachtaí a athsheoladh le níos lú romha. - Comhcheangail de dhá ról ROUTER agus CLIENT. Ní do ghléasanna soghluaiste. - Ceannaire infreastruchtúrtha chun clúdach líonra a leathnú trí theachtaireachtaí a athsheoladh. Infheicthe i liosta na nóid. - Bíonn sé ag seoladh pacáistí teiliméadair mar thosaíocht. - Optamaithe le haghaidh cumarsáide ATAK, laghdaíonn sé cainéil seirbhíse beacht. - Ceadaíonn fadhb beathú do bhoganna PLI i sórtú PLI i feidhm reatha. - Bíonn sé ag seoladh pacáistí suíomh GPS mar thosaíocht. Fáilte atá faighte le haghaidh niúúáil Gan route Faighte Am tráth Táscaire Cumhachta Athnuachana Aithint an Aoise, tomhas a úsáidtear chun leibhéal cumhachta atá faighte ag an antsnáithe a mheas. Léiríonn RSSI níos airde gnóthachtáil níos laige atá i gceangal seasmhach agus níos láidre. - Sábháil Sábháil @@ -257,7 +233,6 @@ Roghnaigh go léir Seol - Roinn @@ -296,7 +271,6 @@ Ainm Úsáideora Anaithnid Neamh-aithnidiúil - trí MQTT Scrios an pointe bealach? diff --git a/core/resources/src/commonMain/composeResources/values-gl/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-gl/schema_strings.xml new file mode 100644 index 0000000000..13bb662e55 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-gl/schema_strings.xml @@ -0,0 +1,26 @@ + + + + + Cancelar + Nome + Cliente + Rexión + Mensaxe + Gardar + diff --git a/core/resources/src/commonMain/composeResources/values-gl/strings.xml b/core/resources/src/commonMain/composeResources/values-gl/strings.xml index f412cacdb8..c57a375eab 100644 --- a/core/resources/src/commonMain/composeResources/values-gl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-gl/strings.xml @@ -25,13 +25,11 @@ Engadir Engadir - Actualización da aplicación requerida Aplicar Está seguro de que quere cambiar á canle predeterminada? - Calculando… Permiso da cámara @@ -39,7 +37,6 @@ Non se puido cambiar de canle, porque a radio aínda non está conectada. Por favor inténteo de novo. A ligazón desta canle non é válida e non pode usarse - Canle Nome de canle Escoller tema @@ -72,15 +69,13 @@ Mensaxe directa Desconectado - Distancia + Editar 8 Horas - - Erro O límite do Ciclo de Traballo de Sinal foi alcanzado. Non se pode enviar mensaxes agora, inténtao despois. @@ -93,8 +88,6 @@ Actualización fallou - - Ignorar Engadir '%1$s' á lista de ignorar? Quitar '%1$s' da lista de ignorar? @@ -114,7 +107,6 @@ Bloqueado - Capacidade de Caché: %1$d MB\nUso de Caché: %2$d MB Xestor de caché Tamaño de caché actual %1$d 'tiles' @@ -167,19 +159,19 @@ Eliminar Ningún (desactivado) Non conectado + OK 24 Horas - + - Linguaxe Predeterminado do sistema @@ -193,8 +185,6 @@ Nova conversa rápida Configuración de radio Reiniciar - - Rexión Eliminar @@ -206,9 +196,7 @@ Restablecer a por defecto Cliente - Aplicación conectada ou dispositivo de mensaxería autónomo. - Gardar Gardar @@ -216,7 +204,6 @@ Seleccionar todo Enviar - Compartir @@ -242,7 +229,6 @@ Nome de usuario descoñecido Non recoñecido - vía MQTT Eliminar punto de ruta? diff --git a/core/resources/src/commonMain/composeResources/values-he/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-he/schema_strings.xml new file mode 100644 index 0000000000..f12854363a --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-he/schema_strings.xml @@ -0,0 +1,25 @@ + + + + + בטל + שם + אזור + הודעה + שמור + diff --git a/core/resources/src/commonMain/composeResources/values-he/strings.xml b/core/resources/src/commonMain/composeResources/values-he/strings.xml index 0633d5f95f..48f589399a 100644 --- a/core/resources/src/commonMain/composeResources/values-he/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-he/strings.xml @@ -25,13 +25,11 @@ הוסף הוסף - נדרש עדכון של האפליקציה החל לשנות לערוץ ברירת המחדל? - הגדרות מחשב… @@ -41,7 +39,6 @@ כיבוי אינו נתמך במכשיר זה כתובת ערוץ זה אינו תקין ולא ניתן לעשות בו שימוש - ערוץ שם הערוץ בחר ערכת עיצוב @@ -71,13 +68,11 @@ מנותק הודעות - מרחק + - - שגיאה הגעת לרף ה-duty cycle. לא ניתן לשלוח הודעות כרגע, בבקשה נסה שוב מאוחר יותר. @@ -90,8 +85,6 @@ העדכון נכשל - - התעלם הוסף '%1$s' לרשימת ההתעלמות? המכשיר יתחיל מחדש. הורד '%1$s' מרשימת ההתעלמות? המכשיר יתחיל מחדש. @@ -111,7 +104,6 @@ נעול - מקום אחסון מטמון: %1$dMB\nמטמון משומש: %2$dMB ניהול מטמון גודל מטמון נוכחי %1$d אזורי מפה @@ -159,18 +151,18 @@ לא מחובר (כבוי) לא מחובר + אישור - + - שפה ברירות מחדל @@ -184,8 +176,6 @@ צ'ט מהיר חדש הגדרות רדיו אתחול מחדש - - אזור דווח @@ -195,7 +185,6 @@ איפוס לברירת מחדל - שמור שמור @@ -203,7 +192,6 @@ בחר הכל שלח - הגדרות שתף @@ -227,7 +215,6 @@ שם המשתמש אינו מוכר - מחק נקודת ציון? ערוך נקודת ציון diff --git a/core/resources/src/commonMain/composeResources/values-hr/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-hr/schema_strings.xml new file mode 100644 index 0000000000..b1ed2a5684 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-hr/schema_strings.xml @@ -0,0 +1,30 @@ + + + + + Crveno + Zadano + Odustani + Ime + Regija + Poruka + Spremi + Zadano + Zadano + Crveno + diff --git a/core/resources/src/commonMain/composeResources/values-hr/strings.xml b/core/resources/src/commonMain/composeResources/values-hr/strings.xml index c3e1f049aa..d459661805 100644 --- a/core/resources/src/commonMain/composeResources/values-hr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-hr/strings.xml @@ -25,7 +25,6 @@ Dodaj Dodaj - Potrebna je nadogradnja aplikacije Potvrdi @@ -33,7 +32,6 @@ Jeste li sigurni da želite promijeniti na zadani kanal? Zvuk Postavke Zvuka - Postavke Bluetootha Izračunavanje… @@ -42,7 +40,6 @@ Nije moguće promijeniti kanal jer radio još nije povezan. Molim pokušajte ponovno. Ovaj URL kanala je nevažeći i ne može se koristiti - Kanal Naziv kanala Odaberi temu @@ -76,15 +73,13 @@ Izravna poruka Odspojeno - Udaljenost + Uredi 8 Sati - - Pogreška Dosegnuto je ograničenje radnog ciklusa. Trenutačno nije moguće poslati poruke, pokušajte ponovno kasnije. @@ -97,8 +92,6 @@ Ažuriranje neuspjelo - - Ignoriraj Dodati '%1$s' na popis ignoriranih? Vaš radio će se ponovno pokrenuti nakon ove promjene. Ukloniti '%1$s' s popisa ignoriranih? Vaš radio će se ponovno pokrenuti nakon ove promjene. @@ -117,7 +110,6 @@ Zaključano - Kapacitet predmemorije: %1$d MB\nUpotreba predmemorije: %2$d MB Upravitelj predmemorije Trenutna veličina predmemorije %1$d dijelova karte @@ -173,19 +165,19 @@ Ukloni Ništa (onemogućeno) Nije povezano + U redu 24 Sati - + - Jezik Zadana vrijednost sustava @@ -199,9 +191,6 @@ Novi brzi razgovor Konfiguracija uređaja Ponovno pokreni - - Crveno - Regija Ukloni @@ -214,7 +203,6 @@ Potvrđeno - Spremi Spremi @@ -222,7 +210,6 @@ Označi sve Potvrdi - Podijeli @@ -231,7 +218,6 @@ Obriši Utišaj - Crveno Tema Tamna Svijetla @@ -248,7 +234,6 @@ Nepoznati korisnik - putem MQTT Obriši putnu točku? diff --git a/core/resources/src/commonMain/composeResources/values-ht/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ht/schema_strings.xml new file mode 100644 index 0000000000..eba2b9a347 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ht/schema_strings.xml @@ -0,0 +1,39 @@ + + + + + Anile + Pa gen + Non + Menm jan ak konpòtman kòm "ALL" men sote dekodaj pakè yo epi senpleman rebroadcast yo. Disponib sèlman nan wòl Repeater. Mete sa sou nenpòt lòt wòl ap bay konpòtman "ALL". + Ignoré mesaj obsève soti nan meshes etranje ki louvri oswa sa yo li pa ka dekripte. Sèlman rebroadcast mesaj sou kanal prensipal / segondè lokal nœud. + Pa gen + Sèlman pèmèt pou wòl SENSOR, TRACKER ak TAK_TRACKER, sa a ap entèdi tout rebroadcasts, pa diferan de wòl CLIENT_MUTE. + Aparèy ki sèlman voye kòm sa nesesè pou kachèt oswa ekonomi pouvwa. + Aparèy ki pa voye pake soti nan lòt aparèy. + Voye pozisyon kòm mesaj nan kanal default regilyèman pou ede ak rekiperasyon aparèy. + Voye pakè telemetri kòm priyorite. + Optimizé pou kominikasyon sistèm ATAK, redwi emisyon regilye. + Pèmèt emisyon TAK PLI otomatik epi redwi emisyon regilye. + Voye pakè pozisyon GPS kòm priyorite. + Rejyon + Mesaj + Pa gen + Sove + Tan pase + diff --git a/core/resources/src/commonMain/composeResources/values-ht/strings.xml b/core/resources/src/commonMain/composeResources/values-ht/strings.xml index 299ebe81a2..9c6adbf202 100644 --- a/core/resources/src/commonMain/composeResources/values-ht/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ht/strings.xml @@ -17,6 +17,8 @@ --> + Imidite + Tanperati Sou Aksepte @@ -27,7 +29,6 @@ Administrasyon Pousantaj tan lè transmisyon te itilize nan dènye èdtan an. - Aplikasyon twò ansyen Aplike @@ -35,7 +36,6 @@ Bon Eske ou sèten ou vle chanje pou kanal default la? Move - Batri Ap kalkile… @@ -46,7 +46,6 @@ Itilizasyon pou kanal aktyèl la, ki enkli TX, RX byen fòme ak RX mal fòme (sa yo rele bri). Kanal URL sa a pa valab e yo pa kapab itilize li - kanal Non kanal Chwazi tèm @@ -77,17 +76,15 @@ Dekonekte Direk - Distans + Modifye 8 Èdtan - Pa matche kle piblik Chifreman Kle Piblik - Erè Limit sik devwa rive. Pa ka voye mesaj kounye a, tanpri eseye ankò pita. @@ -102,10 +99,8 @@ Bon - Hops Lwen (Kalite Lèy Entèryè) echèl relatif valè IAQ jan li mezire pa Bosch BME680. Ranje valè 0–500. - Ignoré Ajoute '%1$s' nan lis ignòre? Retire '%1$s' nan lis ignòre? @@ -126,7 +121,6 @@ Jounal - Kapasite Kach: %1$d MB\nItilizasyon Kach: %2$d MB Manadjè Kach Gwosè Kach aktyèl %1$d tèk @@ -184,19 +178,19 @@ Okenn (enfim) Pa gen Pa konekte + Dakò 24 Èdtan - + - Lang Sistèm default @@ -210,14 +204,6 @@ Nouvo chat rapid Konfigirasyon radyo Rekòmanse - - Rebroadcast nenpòt mesaj obsève, si li te sou kanal prive nou oswa soti nan yon lòt mesh ak menm paramèt lora. - Menm jan ak konpòtman kòm "ALL" men sote dekodaj pakè yo epi senpleman rebroadcast yo. Disponib sèlman nan wòl Repeater. Mete sa sou nenpòt lòt wòl ap bay konpòtman "ALL". - Ignoré pakè soti nan portnum ki pa estanda tankou: TAK, RangeTest, PaxCounter, elatriye. Sèlman rebroadcast pakè ak portnum estanda: NodeInfo, Tèks, Pozisyon, Telemetri, ak Routing. - Ignoré mesaj obsève soti nan meshes etranje tankou "LOCAL ONLY", men ale yon etap pi lwen pa tou ignorer mesaj ki soti nan nœud ki poko nan lis konnen nœud la. - Ignoré mesaj obsève soti nan meshes etranje ki louvri oswa sa yo li pa ka dekripte. Sèlman rebroadcast mesaj sou kanal prensipal / segondè lokal nœud. - Sèlman pèmèt pou wòl SENSOR, TRACKER ak TAK_TRACKER, sa a ap entèdi tout rebroadcasts, pa diferan de wòl CLIENT_MUTE. - Rejyon Administrasyon Remote @@ -230,23 +216,12 @@ Reyajiste Reyajiste nan paramèt default yo - Aplikasyon konekte oswa aparèy mesaj endepandan. - Aparèy ki sèlman voye kòm sa nesesè pou kachèt oswa ekonomi pouvwa. - Aparèy ki pa voye pake soti nan lòt aparèy. - Nœud enfrastrikti pou elaji kouvèti rezo pa relaye mesaj avèk ti overhead. Pa vizib nan lis nœud. - Kombinasyon de toude ROUTER ak CLIENT. Pa pou aparèy mobil. - Nœud enfrastrikti pou elaji kouvèti rezo pa relaye mesaj. Vizyèl nan lis nœud. - Voye pakè telemetri kòm priyorite. - Optimizé pou kominikasyon sistèm ATAK, redwi emisyon regilye. - Pèmèt emisyon TAK PLI otomatik epi redwi emisyon regilye. - Voye pakè pozisyon GPS kòm priyorite. Rekòmanse avèk yon refi negatif Pa gen wout Rekonekte Tan pase Endikatè Fòs Siynal Resevwa, yon mezi ki itilize pou detèmine nivo pouvwa siynal ki resevwa pa antèn nan. Yon RSSI pi wo jeneralman endike yon koneksyon pi fò ak plis estab. - Sove Sove @@ -254,7 +229,6 @@ Chwazi tout Voye - Pataje @@ -284,7 +258,6 @@ Non itilizatè enkoni Inkonu - atravè MQTT Efase pwen? diff --git a/core/resources/src/commonMain/composeResources/values-hu/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-hu/schema_strings.xml new file mode 100644 index 0000000000..85f4ebfbb3 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-hu/schema_strings.xml @@ -0,0 +1,244 @@ + + + + + Kék + Áramerősség + Zöld + LED állapot + Piros + Alapértelmezett + CODEC2 mintavételi ráta + CODEC 2 engedélyezve + I2S adat ki + I2S órajel + I2S adat be + I2S szóválasztás + PTT pin + Bluetooth engedélyezve + Párosítási mód + Bemeneti esemény generálása óramutató járásával ellentétes irányban + Bemeneti esemény generálása óramutató járásával megegyező irányban + Bemeneti esemény generálása nyomáskor + GPIO láb az A porthoz (forgóenkóder) + GPIO láb a B porthoz (forgóenkóder) + GPIO láb a nyomógomb porthoz (forgóenkóder) + Megszakítani + Semmi + 1. forgóenkóder engedélyezve + Harangjel küldése + Fel/Le/Kiválaszt gomb engedélyezve + Érzékelési ravasztípus + Érzékelő szenzor engedélyezve + Figyelt GPIO láb + Barátságos név + Harangjel küldése riasztási üzenettel + INPUT_PULLUP mód használata + Gomb GPIO + Csipogó (buzzer) GPIO + Dupla koppintás mint gomb + A támogatott gyorsulásmérők dupla koppintását kezelje felhasználói gombnyomásként. + LED ütemjelzés + Csomópont-információ sugárzási időköze + Újrasugárzási mód + Összes + Minden dekódolás kihagyása + Ugyanaz, mint az „ALL” viselkedés, de kihagyja a csomag dekódolását és egyszerűen újrasugározza. Csak Ismétlő (Repeater) szerepkörben elérhető; más szerepkörben az „ALL” mód érvényesül. + Csak alap portszámok + Csak ismert + Csak helyi + Figyelmen kívül hagyja a nyílt vagy nem dekódolható idegen hálózatok üzeneteit. Csak a csomópont helyi elsődleges / másodlagos csatornáin sugároz újra. + Semmi + Csak SENSOR, TRACKER és TAK_TRACKER szerepkörben engedélyezett; minden újraküldést letilt, hasonlóan a CLIENT_MUTE szerephez. + Eszköz szerepköre + Kliens + Rejtett Kliens + Eszköz, amely csak szükség esetén sugároz, rejtettség vagy energiatakarékosság miatt. + Néma Kliens + Olyan eszköz, amely nem továbbít más eszközöktől érkező csomagokat. + Elveszett és Megkerült + Rendszeresen sugározza a helyzetet az alapértelmezett csatornára az eszköz visszakeresésének segítésére. + Jelismétlő + Router + Router Kliens + Késő Router + Szenzor + Telemetriai csomagok elsődleges sugárzása. + TAK + ATAK rendszerkommunikációra optimalizált, csökkenti a rutin-sugárzásokat. + TAK Tracker + Automatikus TAK PLI sugárzást engedélyez és csökkenti a rutin-sugárzásokat. + Tracker + GPS-pozíció csomagok elsődleges sugárzása. + Időzóna + Karusszel időköz + Automatikusan a következő oldalra vált a kijelzőn, karusszelszerűen, a megadott időköz szerint. + Mindig észak felé mutasson + A képernyőn, a körön kívül megjelenő iránytű mutatója mindig észak felé mutat. + Iránytű tájolás + Kijelző mód + Az alapértelmezett képernyőelrendezés felülbírálása. + Kijelző megfordítása + Képernyő megfordítása függőlegesen. + Félkövér címsor + A címszöveg félkövér megjelenítése a képernyőn. + OLED típus + Az automatikus OLED-kijelző-felismerés felülbírálása. + Kijelző bekapcsolva ennyi ideig + Meddig marad bekapcsolva a kijelző a gomb megnyomása vagy üzenet érkezése után. + Mértékegységek megjelenítése + A kijelzőn megjelenített mértékegységek. + 12 órás időformátum használata + Engedélyezéskor az eszköz 12 órás formátumban jeleníti meg az időt a kijelzőn. + Érintésre vagy mozgásra ébresztés + Gyorsulásmérő jelenlétét igényli az eszközön. + Kimeneti LED aktív magas szint + Riasztási harang LED + Riasztási harang csipogó + Riasztási harang rezgés + Riasztási üzenet LED + Riasztási üzenet csipogó + Riasztási üzenet rezgés + Külső értesítés engedélyezve + Kimeneti LED (GPIO) + Kimeneti csipogó (GPIO) + Kimeneti rezgő (GPIO) + I2S használata csipogóként + PWM csipogó használata + Sávszélesség + Frekvencia sáv + A csomópont működési frekvenciája a régió, a modem-előbeállítás és ezen mező alapján kerül kiszámításra. Ha az érték 0, a slot automatikusan kiszámításra kerül az elsődleges csatorna neve alapján, és eltér a nyilvános alapértelmezett slottól. Állítsa vissza a nyilvános alapértelmezett slotra, ha privát elsődleges és nyilvános másodlagos csatornák vannak beállítva. + Kódolási ráta + MQTT-re továbbítható + Ugrások száma + A maximális ugrásszám beállítása, alapértelmezés: 3. Az ugrások növelése növeli a hálózati terhelést, ezért körültekintően használja. A 0 ugrásos (0 hop) műsorszórt üzenetek nem kapnak visszaigazolást (ACK). + MQTT figyelmen kívül hagyása + Előbeállítások + Nagy hatótáv – Gyors + Nagy hatótáv – Közepes + Nagy hatótáv – Lassú + Long Range - Turbo + Közepes hatótáv – Gyors + Közepes hatótáv – Lassú + Kis hatótáv – Gyors + Kis hatótáv – Lassú + Kis hatótáv – Turbó + Nagyon nagy hatótáv – Lassú + Duty Cycle felülbírálása + Frekvencia felülbírálása + PA ventilátor letiltva + Régió + A régió, ahol a rádiókat használni fogja. + Szórási Faktor + RX fokozott erősítés + Adás engedélyezve + Adásteljesítmény + Előbeállítás használata + Üzenet + Cím + MQTT engedélyezve + Titkosítás engedélyezve + Jelszó + Proxy kliens felé engedélyezve + Gyökér téma + Felhasználónév + Szomszéd-információ engedélyezve + Továbbítás LoRa-n keresztül + Meghatározza, hogy az MQTT-n és a PhoneAPI-n kívül a NeighborInfo továbbítva legyen-e LoRa-n is. Nem érhető el alapértelmezett kulcsú és nevű csatornán. + Frissítési időköz + IPv4 mód + Csomagok sugárzásának engedélyezése UDP-n a helyi hálózaton. + Ethernet engedélyezve + Az Ethernet engedélyezése letiltja az alkalmazáshoz tartozó Bluetooth-kapcsolatot. A TCP csomópont-kapcsolatok Apple eszközökön nem érhetők el. + Átjáró + IP + Alhálózat + NTP szerver + Semmi + UDP sugárzás + rsyslog szerver + A Wi-Fi engedélyezése letiltja az alkalmazáshoz tartozó Bluetooth-kapcsolatot. + Jelszó + SSID + BLE RSSI küszöbérték (alapértelmezés: -80) + Paxcounter engedélyezve + Frissítési időköz + Minimális távolság + Az intelligens pozícióközvetítéshez figyelembe vett minimális távolságváltozás (méterben). + Minimális időköz + Rögzített pozíció + GPS engedély (EN) GPIO + GPS mód (fizikai hardver) + Frissítési időköz + Milyen gyakran próbáljunk GPS-pozíciót szerezni (< 10 mp alatti érték bekapcsolva tartja a GPS-t). + Engedélyezve + Sugárzási időköz + A maximális időköz, amely pozícióközvetítés nélkül eltelhet egy csomópontnál. + Intelligens pozíció + Pozíció jelzők (flags) + Opcionális mezők a pozícióüzenetek összeállításához. Minél több mezőt tartalmaz az üzenet, annál nagyobb lesz — hosszabb adásidővel és nagyobb csomagvesztési kockázattal. + Magasság + Időbélyeg + GPS vevő (RX) GPIO + GPS adó (TX) GPIO + ADC szorzó felülbírálási arány + Energiatakarékos mód engedélyezése + Mindent a lehető legjobban alvó módba helyez; követő és érzékelő szerepkörben ez a LoRa-rádiót is érinti. Ne használd ezt a beállítást, ha telefonos alkalmazással szeretnéd használni az eszközt, vagy ha az eszközön nincs felhasználói gomb. + Leállítás áramszünet esetén + Bluetooth-várakozás időtartama + Hatótáv-teszt engedélyezve + .CSV mentése a tárhelyre (csak ESP32) + Admin kulcs + Az a nyilvános kulcs, amely jogosult admin üzeneteket küldeni ehhez a csomóponthoz. + Hibakeresési napló API engedélyezve + Élő hibakeresési napló kiírása a soros porton; pozícióadatoktól anonimizált eszköznaplók megtekintése és exportálása Bluetooth-on. + Felügyelt mód + Az eszközt hálózati adminisztrátor kezeli, a felhasználó nem fér hozzá az eszköz beállításaihoz. + Privát kulcs + Nyilvános kulcs + Soros konzol + Soros konzol a Stream API-n keresztül. + Soros baud ráta + Soros baud ráta + Echo engedélyezve + Soros engedélyezve + Soros mód + Alapértelmezett + Alapértelmezett + Időtúllépés + Heartbeat jel + Előzmény-visszaadás maximum + Előzmény-visszaadás időablak + Szerver + Rekordok száma + Szerepkör + Kék + Zöld + Piros + Levegőminőség-metrika modul engedélyezve + Levegőminőségi metrikák frissítési időköze + Eszköztelemetria küldése + Eszközmetrikák frissítési időköze + Környezeti metrikák Fahrenheit-ben + Környezeti metrika modul engedélyezve + Környezeti metrikák megjelenítése képernyőn + Környezeti metrikák frissítési időköze + Energia-metrika modul engedélyezve + Energia-metrikák megjelenítése képernyőn engedélyezve + Tápellátási metrikák frissítési időköze + diff --git a/core/resources/src/commonMain/composeResources/values-hu/strings.xml b/core/resources/src/commonMain/composeResources/values-hu/strings.xml index d6eadf9a43..4f85f322b5 100644 --- a/core/resources/src/commonMain/composeResources/values-hu/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-hu/strings.xml @@ -17,6 +17,8 @@ --> + Páratartalom + Hőmérséklet A programról Elfogadni Visszaigazolások (ACK-ek) @@ -24,7 +26,6 @@ Töröljem az üzenetet Műveletek ADC szorzó felülbírálása - ADC szorzó felülbírálási arány Új hozzáadása Privát jegyzet hozzáadása… @@ -32,7 +33,6 @@ Réteg hozzáadása Új hozzáadása Cím - Admin kulcs Admin kulcsok Adminisztráció Haladó @@ -40,23 +40,13 @@ Haladó Levegőminőség ikon - Levegőminőség-metrika modul engedélyezve - Levegőminőségi metrikák frissítési időköze Az elmúlt órában az adásra használt adásidő százaléka. - - Riasztási harang csipogó - Riasztási harang LED Riasztási harang karakter! - Riasztási harang rezgés - Riasztási üzenet csipogó - Riasztási üzenet LED - Riasztási üzenet rezgés Összes Bemeneti forrás engedélyezése Nem definiált pinek elérésének engedélyezése Magasság Magasság - Mindig észak felé mutasson Környezeti fény Környezeti világítás beállításai Analitikai adatokat gyűjtünk az Android alkalmazás fejlesztésének segítésére (köszönjük). Anonimizált információkat kapunk a felhasználói viselkedésről, beleértve a hibajelentéseket, a használt képernyőket stb. @@ -77,22 +67,13 @@ Hangbeállítások Elérhető pinek Rossz - - Sávszélesség Akkumulátor Akkumulátor INA_2XX I2C-cím - BLE RSSI küszöbérték (alapértelmezés: -80) - Kék Bluetooth Bluetooth beállítások - Bluetooth engedélyezve Bluetooth - Félkövér címsor Beállítások - Sugárzási időköz - Gomb GPIO - Csipogó (buzzer) GPIO Számolás… Kamera hozzáférés engedély Megszakítani @@ -102,7 +83,6 @@ Előre beállított üzenet engedélyezve Nem lehet csatornát váltani, mert a rádió nincs csatlakoztatva. Kérem próbálja meg újra. Leállítás nem támogatott ezen az eszközön - Karusszel időköz A jelenlegi csatorna kihasználtsága, beleértve a megfelelő TX/RX és a hibás RX (zaj) csomagokat. 1. csatorna @@ -115,7 +95,6 @@ 8. csatorna Csatorna funkciók Ez a csatorna URL érvénytelen, ezért nem használható. - Csatorna Csatorna neve Csatornák @@ -133,47 +112,19 @@ Kliens értesítés Bezárás Kijelölés bezárása - CODEC 2 engedélyezve - CODEC2 mintavételi ráta - Kódolási ráta Kommunikálj a barátaiddal és a közösségeddel mobilhálózat nélkül, a hálózaton kívül is. Iránytű észak felül - Iránytű tájolás Kompromittált kulcsok észlelve, válaszd az OK-t az újrageneráláshoz. - A támogatott gyorsulásmérők dupla koppintását kezelje felhasználói gombnyomásként. Szabályozza az eszköz villogó LED-jét. A legtöbb eszköznél legfeljebb 4 LED egyikét vezérli; a töltő és GPS LED nem állítható. - Meghatározza, hogy az MQTT-n és a PhoneAPI-n kívül a NeighborInfo továbbítva legyen-e LoRa-n is. Nem érhető el alapértelmezett kulcsú és nevű csatornán. Elsődleges csatornán pozíció küldése a gomb háromszori megnyomásakor. Időzóna a kijelzőn és a naplóban megjelenő dátumokhoz. A telefon időzónájának használata - Automatikusan a következő oldalra vált a kijelzőn, karusszelszerűen, a megadott időköz szerint. - A képernyőn, a körön kívül megjelenő iránytű mutatója mindig észak felé mutat. - Az alapértelmezett képernyőelrendezés felülbírálása. - Képernyő megfordítása függőlegesen. - A címszöveg félkövér megjelenítése a képernyőn. - Az automatikus OLED-kijelző-felismerés felülbírálása. - Meddig marad bekapcsolva a kijelző a gomb megnyomása vagy üzenet érkezése után. - A kijelzőn megjelenített mértékegységek. - Gyorsulásmérő jelenlétét igényli az eszközön. - A csomópont működési frekvenciája a régió, a modem-előbeállítás és ezen mező alapján kerül kiszámításra. Ha az érték 0, a slot automatikusan kiszámításra kerül az elsődleges csatorna neve alapján, és eltér a nyilvános alapértelmezett slottól. Állítsa vissza a nyilvános alapértelmezett slotra, ha privát elsődleges és nyilvános másodlagos csatornák vannak beállítva. - A maximális ugrásszám beállítása, alapértelmezés: 3. Az ugrások növelése növeli a hálózati terhelést, ezért körültekintően használja. A 0 ugrásos (0 hop) műsorszórt üzenetek nem kapnak visszaigazolást (ACK). Elérhető modem-előbeállítások, alapértelmezés: Nagy hatótáv – Gyors. A régió, ahol a rádiókat használni fogja. - Az Ethernet engedélyezése letiltja az alkalmazáshoz tartozó Bluetooth-kapcsolatot. A TCP csomópont-kapcsolatok Apple eszközökön nem érhetők el. Csomagok sugárzásának engedélyezése UDP-n a helyi hálózaton. - A maximális időköz, amely pozícióközvetítés nélkül eltelhet egy csomópontnál. - Az intelligens pozícióközvetítéshez figyelembe vett minimális távolságváltozás (méterben). - A pozíciófrissítések legnagyobb gyakorisága, ha a minimális távolságváltozás teljesült. - Opcionális mezők a pozícióüzenetek összeállításához. Minél több mezőt tartalmaz az üzenet, annál nagyobb lesz — hosszabb adásidővel és nagyobb csomagvesztési kockázattal. - Milyen gyakran próbáljunk GPS-pozíciót szerezni (< 10 mp alatti érték bekapcsolva tartja a GPS-t). - Mindent a lehető legjobban alvó módba helyez; követő és érzékelő szerepkörben ez a LoRa-rádiót is érinti. Ne használd ezt a beállítást, ha telefonos alkalmazással szeretnéd használni az eszközt, vagy ha az eszközön nincs felhasználói gomb. - Az a nyilvános kulcs, amely jogosult admin üzeneteket küldeni ehhez a csomóponthoz. - Élő hibakeresési napló kiírása a soros porton; pozícióadatoktól anonimizált eszköznaplók megtekintése és exportálása Bluetooth-on. - Az eszközt hálózati adminisztrátor kezeli, a felhasználó nem fér hozzá az eszköz beállításaihoz. Távoli eszközzel közös kulcs létrehozására használatos. - Soros konzol a Stream API-n keresztül. Kritikus riasztások beállítása Helyhozzáférés beállítása Értesítési engedélyek beállítása @@ -209,7 +160,6 @@ Összes szűrő törlése Szűrő hozzáadva Szűrők - Hibakeresési napló API engedélyezve Naplók exportálása Hibakereső panel Keresés törlése @@ -234,8 +184,6 @@ Részletek Érzékelő szenzor Érzékelő szenzor beállításai - Érzékelő szenzor engedélyezve - Érzékelési ravasztípus Eszköz Eszközkonfiguráció @@ -243,9 +191,7 @@ Az ezen a telefonon megtartandó eszköz-adatbázisok maximális száma Eszköz GPS-e Eszközmetrikák - Eszközmetrikák frissítési időköze Az eszköz alszik - Eszköztelemetria küldése Harmatpont Közvetlen üzenet Közvetlen üzenet kulcsa @@ -259,11 +205,7 @@ Üzenetek Kijelölve Szabad lemezterület %1$d - Kijelző - Kijelző mód - Engedélyezéskor az eszköz 12 órás formátumban jeleníti meg az időt a kijelzőn. - Mértékegységek megjelenítése Távolság Távolságszűrők @@ -274,32 +216,22 @@ Keresés törlése MQTT Csomópontok - Dupla koppintás mint gomb A nyilvános internetes átjáróból érkező üzenetek továbbításra kerülnek a helyi mesh hálózatra. A zéró-ugrási szabály miatt az alapértelmezett MQTT szerver forgalma nem terjed tovább ennél az eszköznél. Letöltés + Dinamikus Egyszerűen állíts be privát mesh hálózatokat a biztonságos és megbízható kommunikáció érdekében távoli területeken. - Echo engedélyezve Szerkesztés 8 óra - Energiatakarékos mód engedélyezése Engedélyezve - - Titkosítás engedélyezve Publikus kulcs nem egyezik Publikus Kulcs Titkosítás Környezeti metrikák - Környezet - Környezeti metrika modul engedélyezve - Környezeti metrikák megjelenítése képernyőn - Környezeti metrikák frissítési időköze - Környezeti metrikák Fahrenheit-ben Hiba Elérte a Duty Cycle korlátot. Most nem lehet üzenetet küldeni, próbáld újra később. Ethernet beállítások - Ethernet engedélyezve Ethernet IP: Pozíciócsere Lejárat @@ -308,7 +240,6 @@ Minden csomag exportálása Külső értesítés Külső értesítés beállításai - Külső értesítés engedélyezve Gyári beállítások visszaállítása Megfelelő Meshtastic %1$s @@ -324,61 +255,33 @@ Firmware frissítés szükséges. A frissítés sikertelen Firmware-verzió: %1$s - Rögzített PIN Rögzített pozíció - Kijelző megfordítása További információért lásd az adatvédelmi irányelveinket. Szabad memória Frekvencia - Frekvencia sáv - Barátságos név Gázellenállás - Átjáró - Bemeneti esemény generálása óramutató járásával ellentétes irányban - Bemeneti esemény generálása óramutató járásával megegyező irányban - Bemeneti esemény generálása nyomáskor Kezdjük el Jó - GPIO GPIO láb - GPIO láb az A porthoz (forgóenkóder) - GPIO láb a B porthoz (forgóenkóder) - GPIO láb a nyomógomb porthoz (forgóenkóder) - Figyelt GPIO láb - GPS engedély (EN) GPIO - GPS mód (fizikai hardver) - GPS vevő (RX) GPIO - GPS adó (TX) GPIO - Zöld Hardver Hardvermodell Irány - Heartbeat jel Réteg elrejtése Jelszó elrejtése - Előzmény-visszaadás maximum - Előzmény-visszaadás időablak - Ugrások száma Ugrás Messzire Gazdagép Elfogadom. Elolvastam és megértettem a fentieket. Önkéntesen hozzájárulok, hogy a csomópontom adatai titkosítatlanul kerüljenek továbbításra MQTT-n keresztül Tudom, mit csinálok. - I2S órajel - I2S adat be - I2S adat ki - I2S szóválasztás IAQ (Beltéri levegőminőség) relatív IAQ érték a Bosch BME680 szenzor alapján. Értéktartomány: 0–500. Ikonmagyarázatok - Mellőzés Hozzáadod „%1$s”-t a figyelmen kívül hagyási listához? Bejövő figyelmen kívül hagyása - MQTT figyelmen kívül hagyása Eltávolítod „%1$s”-t a figyelmen kívül hagyási listáról? Beállítás importálása @@ -395,7 +298,6 @@ IP IP cím: Port: - IPv4 mód JSON kimenet engedélyezve Szűrés az utolsó észlelés ideje szerint: %1$s @@ -404,8 +306,6 @@ Legújabb stabil Szélesség - LED ütemjelzés - LED állapot Régi admin csatorna Ennek az opciónak az engedélyezése letiltja a titkosítást, és nem kompatibilis az alapértelmezett Meshtastic hálózattal. @@ -442,11 +342,9 @@ Lux Egyéni csempeforrások kezelése Térképrétegek kezelése - Felügyelt mód Kézi pozíciólekérés szükséges Mesh-térkép - Gyorsítótár kapacitása: %1$d MB\nGyorsítótár kihasználtsága: %2$d MB Gyorsítótár kezelő Gyorsítótár mérete jelenleg %1$d csempe @@ -461,11 +359,8 @@ Offline kezelő SQL gyorsítótár kiürítése sikertelen, a részleteket lásd a logcat-ben SQL gyorsítótár kiürítve %1$s számára - Térképadat-jelentés Hozzájárulás a csomópont titkosítatlan adatainak MQTT-n keresztüli megosztásához E funkció engedélyezésével tudomásul veszed és kifejezetten hozzájárulsz ahhoz, hogy az eszköz valós idejű földrajzi helyzete titkosítás nélkül kerüljön továbbításra az MQTT protokollon keresztül. Ez a helyadat felhasználható például élő térképes jelentéshez, eszközkövetéshez és egyéb telemetriai funkciókhoz. - Térképadat-jelentés intervalluma (másodperc) - A csomópont időszakosan titkosítatlan térképadat-csomagot küld a beállított MQTT szerverre, amely tartalmazza az azonosítót, a hosszú és rövid nevet, a hozzávetőleges helyet, a hardvermodellt, a szerepkört, a firmware-verziót, a LoRa régiót, a modem-előbeállítást és az elsődleges csatorna nevét. Válassz letöltési régiót Letöltés indítása irányszög: %1$d° távolság: %2$s @@ -505,7 +400,6 @@ Elküldésre vár Ismeretlen Üzenetek - Minimális sugárzási idő (másodperc) Minimális ébrenléti idő Előbeállítások Modul beállítások @@ -514,7 +408,6 @@ MQTT MQTT beállítások - MQTT engedélyezve Csatlakoztatva Szétkapcsolva Be kell állítania egy régiót @@ -528,14 +421,12 @@ Némítva ennyi ideig: %1$d nap, %2$s óra Némítva: %1$s óra Nincs némítva - Ismétlő riasztás időkorlát (másodperc) Név A név nem lehet üres. Vissza Belépés Szomszéd-információ Szomszéd-információ beállításai - Szomszéd-információ engedélyezve Hálózat Új csatorna URL érkezett Új üzenetek lent @@ -570,7 +461,6 @@ Kedvencek szerint MQTT-n Keresztül NodeDB törlése - Csomópont-információ sugárzási időköze Csomópontok Csomópontok ezen a helyen @@ -580,6 +470,7 @@ Semmi Nincs kapcsolat Jegyzetek + A Meshtastic értesítésekkel tájékoztat az új üzenetekről és más fontos eseményekről. Az értesítési engedélyeket bármikor módosíthatod a beállításokban. Értesítések a csatorna- és közvetlen üzenetekről. @@ -588,12 +479,8 @@ Értesítés riasztás/harang érkezésekor Értesítés üzenet érkezésekor Most - NTP szerver - Rekordok száma - MQTT-re továbbítható OK - OLED típus 24 óra 1 óra @@ -603,25 +490,14 @@ Beállítások megnyitása Beállítások Északra tájolás - - Kimeneti csipogó (GPIO) - Kimeneti időtartam (ezredmásodperc) - Kimeneti LED aktív magas szint - Kimeneti LED (GPIO) - Kimeneti rezgő (GPIO) További menü Konzol soros port felülbírálása - Duty Cycle felülbírálása - Frekvencia felülbírálása - PA ventilátor letiltva - Párosítási mód Jelszó PAX PaxCounter Paxcounter beállításai - Paxcounter engedélyezve Időszakos pozíció-sugárzás A Meshtastic-nek engedélyezni kell a „Közeli eszközök” hozzáférést, hogy Bluetooth-on keresztül eszközöket találjon és csatlakozzon. Használaton kívül kikapcsolható. @@ -633,20 +509,16 @@ %1$d óra %1$d óra + Pozíció Beállítás a telefon jelenlegi helyzete alapján Pozíció engedélyezve - Pozíció jelzők (flags) Pozíció Pozíciócsomag - Energia Energia-beállítások Tápellátási metrikák - Energia-metrika modul engedélyezve - Energia-metrikák megjelenítése képernyőn engedélyezve - Tápellátási metrikák frissítési időköze Pontos helymeghatározás Nyelv Alapbeállítás @@ -655,12 +527,9 @@ Elsődleges Időszakos pozíció- és telemetria-sugárzás - Privát kulcs Pozíció hozzáférés a mesh számára A szolgáltató neve már létezik. - Proxy kliens felé engedélyezve PSK - PTT pin Nyilvános kulcs Nyilvános kulcs megváltozott QR kód @@ -676,22 +545,11 @@ Eszköz beállítások Hatótáv-teszt Hatótáv-teszt beállításai - Hatótáv-teszt engedélyezve Reagálás Újraindítás - - Újrasugárzási mód - Újrasugároz minden észlelt üzenetet, ha az a privát csatornánkon volt, vagy más, azonos LoRa-paraméterű hálózatból származik. - Ugyanaz, mint az „ALL” viselkedés, de kihagyja a csomag dekódolását és egyszerűen újrasugározza. Csak Ismétlő (Repeater) szerepkörben elérhető; más szerepkörben az „ALL” mód érvényesül. - Figyelmen kívül hagyja a nem szabványos portszámú csomagokat (pl. TAK, RangeTest, PaxCounter), és csak a szabványos portszámúakat sugározza újra: NodeInfo, Text, Position, Telemetry, Routing. - Hasonló a „LOCAL ONLY”-hoz, de tovább megy: figyelmen kívül hagyja az olyan csomópontok üzeneteit is, amelyek nem szerepelnek az ismert listában. - Figyelmen kívül hagyja a nyílt vagy nem dekódolható idegen hálózatok üzeneteit. Csak a csomópont helyi elsődleges / másodlagos csatornáin sugároz újra. - Csak SENSOR, TRACKER és TAK_TRACKER szerepkörben engedélyezett; minden újraküldést letilt, hasonlóan a CLIENT_MUTE szerephez. Legutóbbi hálózati eszközök - Piros Biztosan újragenerálod a privát kulcsot?\n\nAzoknak a csomópontoknak, amelyek korábban kulcsot cseréltek ezzel a csomóponttal, el kell távolítaniuk a csomópontot és újra kell cserélniük a kulcsokat a biztonságos kommunikáció helyreállításához. Privát kulcs újragenerálása - Régió Távoli Távoli Adminisztráció @@ -725,30 +583,17 @@ Eszköz szerepköre Kliens - Alkalmazáshoz csatlakoztatott vagy önálló üzenetküldő eszköz. Rejtett Kliens - Eszköz, amely csak szükség esetén sugároz, rejtettség vagy energiatakarékosság miatt. Néma Kliens - Olyan eszköz, amely nem továbbít más eszközöktől érkező csomagokat. Elveszett és Megkerült Jelismétlő - Hálózati lefedettséget bővítő infrastruktúra-csomópont, amely minimális terheléssel továbbítja az üzeneteket. Nem látható a listában. Router Router Kliens - ROUTER és CLIENT kombinációja. Nem hordozható eszközökhöz. - Hálózati lefedettséget bővítő infrastruktúra-csomópont, amely továbbítja az üzeneteket. Látható a csomópont-listában. Késő Router - Infrastruktúra-csomópont, amely minden csomagot egyszer újraküld, de csak az összes más mód után, extra lefedettséget biztosítva a helyi klasztereknek. Látható a listában. Szenzor - Telemetriai csomagok elsődleges sugárzása. TAK - ATAK rendszerkommunikációra optimalizált, csökkenti a rutin-sugárzásokat. TAK Tracker - Automatikus TAK PLI sugárzást engedélyez és csökkenti a rutin-sugárzásokat. Tracker - GPS-pozíció csomagok elsődleges sugárzása. - Gyökér téma - 1. forgóenkóder engedélyezve Negatív visszaigazolás érkezett Nincs út @@ -756,16 +601,12 @@ Időtúllépés RSSI Vett jelerősség-mutató (RSSI): az antenna által vett jel teljesítményszintjének mérése. A magasabb RSSI általában erősebb, stabilabb kapcsolatot jelez. - rsyslog szerver Műholdak - Mentés Mentés - .CSV mentése a tárhelyre (csak ESP32) Hatótáv-teszt csomagok exportálása Keresés - Kijelző bekapcsolva ennyi ideig Görgetés az aljára Másodlagos Az elsődleges csatornán a pozíció letiltása lehetővé teszi az időszakos pozíció-sugárzást az első olyan másodlagos csatornán, ahol a pozíció engedélyezett, ellenkező esetben kézi pozíciólekérés szükséges. @@ -791,17 +632,8 @@ Kijelölve Kiválasztott térképtípus Küldeni - Harangjel küldése - Harangjel küldése riasztási üzenettel - Küldési üzenetintervallum (másodperc) - Soros port - Soros baud ráta Soros beállítások - Soros konzol - Soros engedélyezve - Soros mód - Szerver Régió beállítása beállítások @@ -821,7 +653,6 @@ Útvonalpontok megjelenítése Leállítás Csomópont: %1$s - Leállítás áramszünet esetén ⚠️ Ez LEÁLLÍTJA a csomópontot. Újraindításhoz fizikai beavatkozás szükséges. Jel Jelminőség @@ -829,28 +660,19 @@ Kijelző Kihagyás Sávhely - Intelligens pozíció SNR Jel–zaj arány (SNR): a kommunikációban a kívánt jel szintjének és a háttérzaj szintjének aránya. A Meshtastic és más vezeték nélküli rendszerek esetében a magasabb SNR tisztább jelet jelent, ami javítja az adatátvitel megbízhatóságát és minőségét. Sebesség - Szórási Faktor - SSID - Állapot-sugárzás (másodperc) Maradj kapcsolatban bárhol - Alhálózat Szuper mélyalvás időtartama Támogatott Meshtastic közösség által támogatott Törlés Némítás Némítás feloldása - RX fokozott erősítés Rendszerbeállítások Szerver - Kék - Zöld - Piros Telemetria Telemetria beállítások Téma @@ -858,10 +680,8 @@ Világos Rendszer alapértelmezett Idő - Időzóna Időtúllépés Időbélyeg - TLS engedélyezve Saját pozíció váltása Traceroute @@ -882,7 +702,6 @@ nyomvonalpont - Továbbítás LoRa-n keresztül LoRa MQTT @@ -890,10 +709,10 @@ 24 óra 48 óra 2 hét - Adás engedélyezve - Adásteljesítmény Típus Írj üzenetet + dBm + m Rendszer alapértelmezett @@ -906,8 +725,6 @@ Némítás feloldása Ismeretlen Nincs beállítva – 0 - Fel/Le/Kiválaszt gomb engedélyezve - Frissítési intervallum (másodperc) A mesh üzenetei bármely csomópont beállított átjáróján keresztül kerülnek az internetre. Működési idő @@ -915,25 +732,16 @@ Az URL nem lehet üres. Az URL-nek tartalmaznia kell helyőrzőket. URL sablon - - 12 órás időformátum használata - I2S használata csipogóként - INPUT_PULLUP mód használata - Előbeállítás használata - PWM csipogó használata Felhasználó Felhasználói beállítások Felhasználó-azonosító Felhasználói szöveg - Felhasználónév UV fény (lux) MQTT-n Keresztül Megtekintés térképen Kiadás megtekintése Feszültség - Bluetooth-várakozás időtartama - Érintésre vagy mozgásra ébresztés Figyelmeztetés Útpont törlés? Útpont szerkesztés diff --git a/core/resources/src/commonMain/composeResources/values-is/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-is/schema_strings.xml new file mode 100644 index 0000000000..aa43b1396e --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-is/schema_strings.xml @@ -0,0 +1,25 @@ + + + + + Hætta við + Heiti + Svæði + Skilaboð + Vista + diff --git a/core/resources/src/commonMain/composeResources/values-is/strings.xml b/core/resources/src/commonMain/composeResources/values-is/strings.xml index 688fcaf95a..e32bb61274 100644 --- a/core/resources/src/commonMain/composeResources/values-is/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-is/strings.xml @@ -25,13 +25,11 @@ Bæta við Bæta við - Uppfærsla á smáforriti nauðsynleg Virkja Ert þú viss um að þú viljir skipta yfir á sjálfgefna rás? - Reiknar… Aðgangur að myndavél @@ -66,12 +64,10 @@ Bein skilaboð Aftengd - + - - Hámarsksendingartíma náð. Ekki hægt að senda skilaboð, vinsamlegast reynið aftur síðar. Grunnstilla @@ -82,8 +78,6 @@ Uppfærsla misfórst - - Hunsa Bæta '%1$s' við Ignore lista? Fjarlægja '%1$s' frá hunsa lista? @@ -102,7 +96,6 @@ Læst - Stærð skyndiminnis: %1$d MB\nNýtt skyndiminni: %2$d MB Sýsla með skyndiminni Núverandi stærð skyndiminnis %1$d reitar @@ -140,18 +133,18 @@ Ekkert (Afvirkjað) Ekki tengdur + Í lagi - + - Tungumál Grunnstilling kerfis @@ -165,8 +158,6 @@ Ný flýtiskilaboð Stillingar radíós Endurræsa - - Svæði Tilkynna @@ -176,7 +167,6 @@ Endursetja tæki - Vista Vista @@ -184,7 +174,6 @@ Velja allt Senda - Deila @@ -207,7 +196,6 @@ Óþekkt notendanafn - Eyða leiðarpunkti? Breyta leiðarpunkti diff --git a/core/resources/src/commonMain/composeResources/values-it/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-it/schema_strings.xml new file mode 100644 index 0000000000..4176cae5bc --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-it/schema_strings.xml @@ -0,0 +1,304 @@ + + + + + Blu + Attuale + Verde + LED di stato + Rosso + Predefinito + Frequenza di campionamento CODEC2 + CODEC 2 attivato + I2S data out + I2S clock + I2S data in + I2S word select + Pin PTT + Bluetooth attivo + Modalità abbinamento + Evento generato dalla rotazione in senso antiorario + Evento generato dalla rotazione in senso orario + Evento generato dalla Pressione del pulsante + Pin GPIO della porta A dell'encoder rotativo + Pin GPIO della porta B dell'encoder rotativo + Pin GPIO della porta Pulsante dell'encoder rotativo + Indietro + Annulla + Nessuno + Seleziona + Encoder rotativo #1 abilitato + Invia campanella + Input Su/Giu/Selezione abilitato + Tipo di trigger di rilevamento + Sensore Rilevamento attivo + Pin GPIO da monitorare + Nome semplificato + Invia campanella con messaggio di avviso + Intervallo Di Trasmissione + Alto + Usa modalità INPUT_PULLUP + GPIO del Pulsante + GPIO del Buzzer + Disabilita triplo-click + Doppio tocco come pressione pulsante + Considera il doppio tocco sugli accelerometri supportati come la pressione di un pulsante utente. + Battito Cuore Led + Intervallo Di Trasmissione Info Nodo + Modalità Ritrasmissione + Tutti + Tutto ma Salta Decodifica + Stesso comportamento di ALL ma salta la decodifica dei pacchetti e semplicemente li ritrasmette. Disponibile solo nel ruolo Repeater. Attivando questo su qualsiasi altro ruolo, si otterrà il comportamento di ALL. + Solo Core Portnums + Solo Conosciuti + Solo Locale + Ignora i messaggi osservati da mesh esterne aperte o quelli che non possono essere decifrati. Ritrasmette il messaggio solo nei canali locali primario / secondario dei nodi. + Nessuno + Permesso solo per i ruoli SENSOR, TRACKER e TAK_TRACKER, questo inibirà tutte le ritrasmissioni, come il ruolo CLIENT_MUTE. + Ruolo Del Dispositivo + Client + Base Client + App collegata o dispositivo di messaggistica standalone. + Client Nascosto + Dispositivo che trasmette solo quando necessario, per risparmiare energia o restare invisibile. + Client Mute + Dispositivo che non inoltra pacchetti da altri dispositivi. + Oggetti Smarriti + Trasmette a intervalli regolari la posizione come messaggio nel canale predefinito per aiutare il recupero del dispositivo. + Repeater + Router + Router Client + Router Late + Sensore + Dà priorità alla trasmissione di pacchetti di telemetria. + TAK + Ottimizzato per la comunicazione del sistema ATAK, riduce le trasmissioni di routine. + TAK Tracker + Abilita le trasmissioni automatiche TAK PLI e riduce le trasmissioni di routine. + Tracker + Dà priorità alla trasmissione di pacchetti di posizione GPS. + Fuso Orario + Durata di ogni schermata + Passa automaticamente alla pagina successiva sullo schermo come un carosello, in base all'intervallo specificato. + Punta sempre a nord + La direzione della bussola sullo schermo all'esterno del cerchio punta sempre a nord. + Orientamento bussola + Modalità schermo + Sovrascrivi il layout predefinito dello schermo. + Imperiale + Metrico + Capovolgi schermo + Capovolgi lo schermo verticalmente. + Intestazione in grassetto + Usa il grassetto nelle intestazioni sullo schermo. + Tipo OLED + Ignora il rilevamento automatico dello schermo OLED. + Tieni lo schermo acceso per + Per quanto tempo lo schermo rimane acceso dopo che il pulsante utente viene premuto o i messaggi vengono ricevuti. + Unità di misura visualizzata + Unità di misura visualizzate sullo schermo del dispositivo. + Usa formato orologio 12h + Se abilitato, il dispositivo visualizzerà il tempo in formato 12 ore sullo schermo. + Accendi lo schermo al tocco o al movimento + Richiede la presenza di un accelerometro nel dispositivo. + Output per LED active high + LED campanella di allarme + Buzzer campanella di allarme + Vibrazione campanella di allarme + Avviso messaggi tramite LED + Avviso messaggi tramite suono + Avviso messaggi tramite vibrazione + Notifica esterna attivata + Nag Timeout + LED Output (GPIO) + Output buzzer (GPIO) + Output vibrazione (GPIO) + Usa I2S come buzzer + Usa buzzer PWM + Larghezza di banda + Slot di Frequenza + La frequenza di funzionamento del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Quando è a 0, lo slot viene calcolato automaticamente in base al nome del canale primario e cambierà rispetto allo slot pubblico predefinito. Torna allo slot pubblico predefinito se sono configurati canali primari privati e secondari pubblici. + Coding Rate + OK per MQTT + Numero di Hop + Imposta il numero massimo di hop, il predefinito è 3. Aumentare gli hop comporta anche aumentare la congestione e dovrebbe essere utilizzato con attenzione. Con 0 hop, i messaggi non otterranno conferma di ricezione. + Ignora MQTT + Preset + Lite - Fast + Lite - Slow + Long Range - Fast + Long Range - Moderate + Long Range - Slow + Long Range - Turbo + Medium Range - Fast + Medium Range - Slow + Medium Range - Turbo + Narrow - Fast + Narrow - Slow + Short Range - Fast + Short Range - Slow + Short Range - Turbo + Tiny - Fast + Tiny - Slow + Very Long Range - Slow + Ignora limite di Duty Cycle + Sovrascrivi Frequenza + Ventola PA disabilitata + Regione + La regione in cui utilizzerai le radio. + Cina + India + Giappone + Corea + Russia + Tailandia + Taiwan + Spread Factor + Migliora guadagno in Ricezione + Trasmissione Abilitata + Potenza di Trasmissione + Usa Preset + Osservatore avanzato + Soccorritore + Tiratore scelto + Capo squadra + Membro della squadra + Messaggio + Indirizzo + MQTT abilitato + Crittografia abilitata + Il tuo nodo invierà periodicamente un pacchetto di segnalazione mappa non criptato al server MQTT configurato, questo include id, nome breve e lungo, posizione approssimativa, modello hardware, ruolo, versione del firmware, regione LoRa, preset del modem e nome del canale primario. + Password + Proxy to client attivato + Root topic + TLS abilitato + Username + Info Nodi Vicini abilitato + Trasmettere su LoRa + Se oltre a inviarli tramite MQTT e PhoneAPI, i dati NeighborInfo devono essere trasmessi tramite LoRa. Non disponibile su un canale con chiave e nome predefiniti. + Intervallo Interrogazione GPS + Modalità IPv4 + Abilita la trasmissione di pacchetti tramite UDP sulla rete locale. + Ethernet abilitato + L'attivazione della connessione Ethernet disabiliterà la connessione bluetooth all'app. La connessione al nodo via TCP non è disponibile per i dispositivi Apple. + DNS + Gateway + IP + Subnet + Server NTP + Nessuno + Trasmissione UDP + server rsyslog + WiFi abilitato + L'attivazione della WiFi disabiliterà la connessione bluetooth con l'app. + Password + SSID + Soglia RSSI BLE (valore predefinito -80) + Paxcounter abilitato + Intervallo Interrogazione GPS + Distanza Intelligente + La distanza minima percorsa in metri per essere considerata per la trasmissione in modalità smart position. + Intervallo Intelligente + Posizione Fissa + GPIO EN del GPS + Modalità GPS (Hardware Fisico) + Intervallo Interrogazione GPS + Quanto spesso si tenterà di recuperare una posizione dal GPS (se <10sec il GPS rimarrà sempre attivo). + Abilitato + Intervallo Di Trasmissione + Il tempo massimo che può trascorrere senza che il nodo trasmetta la posizione. + Posizione Smart + Flag Di Posizione + Dati facoltativi da includere nei messaggi di posizione. Più campi sono selezionati, più grande sarà il messaggio, che richiederà maggior tempo di trasmissione e aumenterà il rischio di perdita di pacchetti. + Altitudine + L'altitudine è riferita al livello medio del mare + DOP + Altitudine Separazione Geoidale + Direzione del veicolo + Numero di satelliti + Numero sequenza + Velocità del veicolo + Data e ora + GPIO di Ricezione del GPS + GPIO di Trasmissione del GPS + Sovrascrivi rapporto moltiplicatore ADC + Abilita modalità risparmio energetico + Verranno sospese tutte le funzioni per la maggior parte del tempo. Per i ruoli di tracker e sensor, è inclusa nella sospensione anche la radio lora. Questa configurazione è sconsigliata se il dispositivo viene utilizzato con le app del telefono o se il dispositivo è privo di pulsanti utente. + Spegnimento in mancanza di alimentazione + Durata attesa Bluetooth + Test distanza massima abilitato + Salva .CSV nella memoria (solo ESP32) + Intervallo Del Mittente + Chiave Amministratore + La chiave pubblica che autorizza un nodo a inviare messaggi di amministrazione a questo nodo. + Debug log API abilitato + Produce log di debug in tempo reale su seriale, visualizza ed esporta i log del dispositivo via Bluetooth dopo aver rimosso le informazioni sulla posizione. + Modalità Gestita + Il dispositivo è gestito da un amministratore nella mesh, l'utente non è in grado di accedere a nessuna delle impostazioni del dispositivo. + Chiave Privata + Usato per creare una chiave condivisa con un dispositivo remoto + Chiave Pubblica + Console seriale + Console seriale attraverso la Stream API. + Velocità della seriale + Velocità della seriale + Echo abilitato + Tutti i pacchetti che invii verranno rimandati indietro al tuo dispositivo. + Seriale abilitata + Modalità seriale + RX + 38400 Baud + Predefinito + Predefinito + Timeout + TX + Store & Forward abilitato + Heartbeat + Cronologia ritorno max + Finestra di ritorno cronologia + Server + Numero di record + Ruolo + Blu + Marrone + Ciano + Blu scuro + Verde scuro + Verde + Magenta + Bordeaux + Arancione + Viola + Rosso + Verde acqua + Bianco + Giallo + Modulo metriche della qualità dell'aria abilitato + Intervallo aggiornamento metriche qualità aria + Invia Telemetria Dispositivo + Abilita/disabilita il modulo di telemetria del dispositivo per inviare metriche alla mesh. Questi sono valori nominali: le mesh congestionate passeranno automaticamente a intervalli più lunghi in base al numero di nodi online. + Intervallo aggiornamento metriche dispositivo + Usa i gradi Fahrenheit nelle metriche ambientali + Modulo metriche ambientali abilitato + Metriche ambientali visualizzate su schermo + Intervallo aggiornamento metriche ambientali + Modulo metriche di alimentazione abilitato + Metriche di alimentazione visualizzate su schermo + Intervallo aggiornamento metriche alimentazione + Soglia pacchetti sconosciuti + diff --git a/core/resources/src/commonMain/composeResources/values-it/strings.xml b/core/resources/src/commonMain/composeResources/values-it/strings.xml index 324204f4c3..9547c35415 100644 --- a/core/resources/src/commonMain/composeResources/values-it/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-it/strings.xml @@ -17,17 +17,18 @@ --> + Umidità %1$s: %2$s Messaggio da %1$s: %2$s distante %1$s preferito - %1$d hop di distanza sentito l'ultima volta %1$s offline online ruolo %1$s segnale %1$s %1$d hop: %2$d nodi + Temperatura Informazioni Accetta Ringraziamenti @@ -44,7 +45,6 @@ Traduci messaggio Azioni Sovrascrivi moltiplicatore ADC - Sovrascrivi rapporto moltiplicatore ADC Aggiungere Aggiungi una nota privata… @@ -57,7 +57,6 @@ Aggiungi dispositivo manualmente… Aggiungi Livello Rete Indirizzo - Chiave Amministratore Chiave Amministratore Amministrazione Avanzate @@ -67,24 +66,14 @@ Qualità dell'Aria Icona della qualità dell'aria Registro Metriche Qualità Aria - Modulo metriche della qualità dell'aria abilitato - Intervallo aggiornamento metriche qualità aria Percentuale di tempo di trasmissione utilizzato nell’ultima ora. AirUtil - - Buzzer campanella di allarme - LED campanella di allarme Carattere Campana Di Allarme! - Vibrazione campanella di allarme - Avviso messaggi tramite suono - Avviso messaggi tramite LED - Avviso messaggi tramite vibrazione Tutti Consenti sorgente di input Consenti accesso a pin non definiti Alt Altitudine - Punta sempre a nord Luce Ambientale Configurazione Illuminazione Ambientale I dati di utilizzo sono raccolti per aiutarci a migliorare l'applicazione Android (grazie), riceveremo informazioni anonimizzate sul comportamento dell'utente. Queste includono rapporti di arresti anomali, schermi utilizzate nell'app, ecc. @@ -131,20 +120,15 @@ Salva le chiavi pubbliche e private in una memoria sicura e crittografata su questo dispositivo. Backup & Ripristino Scarso - - Larghezza di banda Pressione atmosferica Batteria Indirizzo INA_2XX I2C della batteria Dispositivi Bluetooth - Soglia RSSI BLE (valore predefinito -80) - Blu Bluetooth Dispositivi Bluetooth Disponibili Configurazione Bluetooth Il Bluetooth è spento. Attivalo per eseguire la scansione dei dispositivi vicini. - Bluetooth attivo Configurazione Gestisci le impostazioni e i canali del dispositivo senza usare cavi. Ricerca @@ -156,14 +140,10 @@ Limite di scansioni Bluetooth raggiunto. Riprova fra %1$d secondo. Limite di scansioni Bluetooth raggiunto. Riprova fra %1$d secondi. - Intestazione in grassetto Accoppiamento non riuscito. Concedi i permessi di ricerca dispositivi nelle vicinanze e riprova. L'accoppiamento non è completato. Riprova l'accoppiamento. Impostazioni - Intervallo Di Trasmissione Alto rumore di fondo - GPIO del Pulsante - GPIO del Buzzer Calcolo… Identificativo di chiamata Autorizzazione fotocamera @@ -176,7 +156,6 @@ Messaggi preconfezionati abilitati Impossibile cambiare il canale, perché la radio non è ancora connessa. Riprova. Spegnimento non supportato su questo dispositivo - Durata di ogni schermata Impegno del canale attuale, inclusi TX e RX ben formati e RX malformati (cioè rumore). Ch @@ -190,7 +169,6 @@ Canale 8 Caratteristiche del canale L'URL di questo Canale non è valida e non può essere usata - Canale Nome del canale URL Canale ChUtil @@ -231,9 +209,6 @@ CO₂ CO₂ Umidità CO₂ Temperatura - CODEC 2 attivato - Frequenza di campionamento CODEC2 - Coding Rate Minimizza grafico Minimizzato Comunica off-grid con i tuoi amici e la tua comunità senza la connessione cellulare. @@ -246,47 +221,22 @@ I permessi di localizzazione sono necessari per mostrare la distanza e la direzione. Questo dispositivo non ha un sensore di bussola. La direzione non è disponibile. Tieni in alto il nord della bussola - Orientamento bussola Bussola Area stimata: \u00b1%1$s (\u00b1%2$s) Area stimata: precisione sconosciuta Rilevate chiavi compromesse, seleziona OK per rigenerarle. - Considera il doppio tocco sugli accelerometri supportati come la pressione di un pulsante utente. Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi questo controllerà uno dei LED (fino a 4), il LED dell'alimentazione e il LED del GPS non sono controllabili. - Se oltre a inviarli tramite MQTT e PhoneAPI, i dati NeighborInfo devono essere trasmessi tramite LoRa. Non disponibile su un canale con chiave e nome predefiniti. Invia la posizione sul canale principale quando il pulsante utente viene cliccato tre volte. Fuso orario per le date sullo schermo del dispositivo e nei log. Usa fuso orario del telefono - Passa automaticamente alla pagina successiva sullo schermo come un carosello, in base all'intervallo specificato. - La direzione della bussola sullo schermo all'esterno del cerchio punta sempre a nord. - Sovrascrivi il layout predefinito dello schermo. - Capovolgi lo schermo verticalmente. - Usa il grassetto nelle intestazioni sullo schermo. - Ignora il rilevamento automatico dello schermo OLED. - Per quanto tempo lo schermo rimane acceso dopo che il pulsante utente viene premuto o i messaggi vengono ricevuti. - Unità di misura visualizzate sullo schermo del dispositivo. - Richiede la presenza di un accelerometro nel dispositivo. - La frequenza di funzionamento del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Quando è a 0, lo slot viene calcolato automaticamente in base al nome del canale primario e cambierà rispetto allo slot pubblico predefinito. Torna allo slot pubblico predefinito se sono configurati canali primari privati e secondari pubblici. - Imposta il numero massimo di hop, il predefinito è 3. Aumentare gli hop comporta anche aumentare la congestione e dovrebbe essere utilizzato con attenzione. Con 0 hop, i messaggi non otterranno conferma di ricezione. I preset di questa regione sono solo per gli operatori con licenza (radioamatori). Abilita Radioamatore con licenza (Ham) nella configurazione utente per selezionarli. Le preimpostazioni del modem disponibili, la predefinita è Long Fast. La regione in cui utilizzerai le radio. - L'attivazione della connessione Ethernet disabiliterà la connessione bluetooth all'app. La connessione al nodo via TCP non è disponibile per i dispositivi Apple. Abilita la trasmissione di pacchetti tramite UDP sulla rete locale. - Il tempo massimo che può trascorrere senza che il nodo trasmetta la posizione. - La distanza minima percorsa in metri per essere considerata per la trasmissione in modalità smart position. - Il tempo minimo tra un invio e l'altro per aggiornare della posizione, se la distanza minima è stata raggiunta. - Dati facoltativi da includere nei messaggi di posizione. Più campi sono selezionati, più grande sarà il messaggio, che richiederà maggior tempo di trasmissione e aumenterà il rischio di perdita di pacchetti. - Quanto spesso si tenterà di recuperare una posizione dal GPS (se <10sec il GPS rimarrà sempre attivo). - Verranno sospese tutte le funzioni per la maggior parte del tempo. Per i ruoli di tracker e sensor, è inclusa nella sospensione anche la radio lora. Questa configurazione è sconsigliata se il dispositivo viene utilizzato con le app del telefono o se il dispositivo è privo di pulsanti utente. Il nodo si sta riavviando e sarà irraggiungibile per qualche secondo. - La chiave pubblica che autorizza un nodo a inviare messaggi di amministrazione a questo nodo. - Produce log di debug in tempo reale su seriale, visualizza ed esporta i log del dispositivo via Bluetooth dopo aver rimosso le informazioni sulla posizione. - Il dispositivo è gestito da un amministratore nella mesh, l'utente non è in grado di accedere a nessuna delle impostazioni del dispositivo. Usata per creare una chiave condivisa con un dispositivo remoto. Generata a partire dalla chiave privata e inviata agli altri nodi della mesh per permettere loro di calcolare una chiave segreta condivisa. - Console seriale attraverso la Stream API. Configurazione Configura Permessi Bluetooth Configura avvisi critici @@ -334,7 +284,6 @@ Filtra inclusi Filtri Preset Filtri - Debug log API abilitato Nessun registro dell'app da visualizzare Aggiorna Esporta i logs @@ -383,8 +332,6 @@ Dettagli Sensore Di Rilevamento Configurazione Sensore Rilevamento - Sensore Rilevamento attivo - Tipo di trigger di rilevamento Dispositivo Configurazione dispositivo @@ -398,12 +345,9 @@ Metriche Dispositivo %1$s %1$s: %2$s%% - Intervallo aggiornamento metriche dispositivo %1$s: %2$s V Il dispositivo è inattivo Archiviazione dispositivo & UI (sola lettura) - Invia Telemetria Dispositivo - Abilita/disabilita il modulo di telemetria del dispositivo per inviare metriche alla mesh. Questi sono valori nominali: le mesh congestionate passeranno automaticamente a intervalli più lunghi in base al numero di nodi online. Tema: %1$s, Lingua: %2$s Punto Di Rugiada Messaggio diretto @@ -485,22 +429,16 @@ Interrompi Scansione Analisi AI non disponibile %1$s rimanenti - %1$d nodi unici Visualizza mappa Disco libero %1$d - Schermo Schermo Dispositivo - Modalità schermo - Se abilitato, il dispositivo visualizzerà il tempo in formato 12 ore sullo schermo. - Unità di misura visualizzata Distanza Filtri distanza Filtra l'elenco dei nodi e la mappa mesh in base alla prossimità al tuo telefono. Misure di distanza Visualizza la distanza tra il telefono e gli altri nodi Meshtastic con posizione attiva. - DNS Azzera ricerca system ai,gemini,assistente,funzioni,automazione,voce @@ -552,38 +490,28 @@ Widget Schermata Home Fatto Non mostrare più per questo dispositivo - Doppio tocco come pressione pulsante I messaggi provenienti da un gateway Internet pubblico vengono inoltrati alla rete mesh locale. A causa della politica zero-hop, il traffico proveniente dal server MQTT predefinito non si propagherà oltre questo dispositivo. Scarica Rilevata chiave pubblica duplicata + Dinamico Crea reti mesh private in modo semplice per connessioni sicure e affidabili in zone remote. - Echo abilitato Modifica Modifica sorgente tile di rete 8 Ore - Abilita modalità risparmio energetico Abilitato - - Crittografia abilitata Chiave pubblica errata La chiave pubblica non corrisponde alla chiave salvata. È possibile rimuovere il nodo e lasciarlo scambiare le chiavi nuovamente, ma questo può indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale attendibile, per determinare se il cambiamento di chiave è dovuto a un ripristino di fabbrica o ad altre azioni intenzionali. Crittografia a Chiave Pubblica Metriche Ambientali - Ambiente - Modulo metriche ambientali abilitato - Metriche ambientali visualizzate su schermo - Intervallo aggiornamento metriche ambientali - Usa i gradi Fahrenheit nelle metriche ambientali Errore Limite di Duty Cycle raggiunto. Impossibile inviare messaggi in questo momento, riprovare più tardi. Impossibile stabilire una connessione stabile dopo ripetuti tentativi. Riseleziona il nodo per riprovare. Connetti & amministra Apertura sessione remota in corso… Opzioni Ethernet - Ethernet abilitato IP Ethernet: Usa tema evento Scambia posizione @@ -597,7 +525,6 @@ Esporta Pacchetto Dati TAK Notifica Esterna Configurazione Notifiche Esterne - Notifica esterna attivata Ripristina impostazioni di fabbrica Discreto Meshtastic %1$s @@ -608,10 +535,6 @@ File disponibili (%1$d): Aggiungi parola o regex:pattern - Disabilita filtro - Abilita Filtro - Abilita filtro - Nascondi messaggi contenenti parole del filtro Nascondi %1$d filtrati Filtro Filtrato @@ -701,6 +624,7 @@ Potrebbe volerci un minuto... Target: %1$s Aggiornamento Firmware + %1$d% Errore sconosciuto Modello hardware sconosciuto: %1$d Versione remota sconosciuta @@ -717,9 +641,7 @@ In attesa che il dispositivo si riavvii in modalità OTA... In attesa che il dispositivo si riconnetta... Versione firmware: %1$s - PIN Fisso Posizione Fissa - Capovolgi schermo Per ulteriori informazioni, consulta la nostra informativa sulla privacy. Grassetto @@ -730,13 +652,7 @@ Memoria libera Memoria di sistema disponibile in byte Freq - Slot di Frequenza - Nome semplificato Resistenza Ai Gas - Gateway - Evento generato dalla rotazione in senso antiorario - Evento generato dalla rotazione in senso orario - Evento generato dalla Pressione del pulsante Genera codice QR Geofence @@ -760,31 +676,17 @@ Imposta l'area sulla mappa Inizia ora Buono - GPIO Pin GPIO - Pin GPIO della porta A dell'encoder rotativo - Pin GPIO della porta B dell'encoder rotativo - Pin GPIO della porta Pulsante dell'encoder rotativo - Pin GPIO da monitorare - GPIO EN del GPS - Modalità GPS (Hardware Fisico) - GPIO di Ricezione del GPS - GPIO di Trasmissione del GPS Concedi il permesso - Verde Hardware Modello hardware Direzione - Heartbeat Aiuto & Documentazione Nascondi livello Nascondi password - Cronologia ritorno max - Finestra di ritorno cronologia Nessun nodo ricevuto in questa finestra Nodi per hop - Numero di Hop Distanza in Hop Host Metriche Host @@ -792,18 +694,12 @@ Sono d’accordo. Ho letto e accetto quanto sopra. Acconsento volontariamente alla trasmissione non crittografata dei dati del mio nodo tramite MQTT So cosa sto facendo. - I2S clock - I2S data in - I2S data out - I2S word select IAQ (Qualità dell'aria interna) scala relativa del valore della qualità dell'aria indoor, misurato da Bosch BME680. Valore Intervallo 0–500. Significato delle icone - Ignora Aggiungere '%1$s' alla lista degli ignorati? Ignora in arrivo - Ignora MQTT Rimuovere '%1$s' dalla lista degli ignorati? Importa configurazione @@ -829,7 +725,6 @@ IP Indirizzo IP: Porta: - Modalità IPv4 Output JSON abilitato %1$s @@ -848,8 +743,6 @@ Latitudine Maggiori informazioni - Battito Cuore Led - LED di stato Canale di Amministrazione legacy %1$d librerie @@ -937,11 +830,9 @@ Lux Gestisci sorgenti Tile personalizzati Gestisci livelli della mappa - Modalità Gestita Richiesta posizione manuale mandatoria Mappa della Mesh - Capacità Cache: %1$d MB\nCache utilizzata: %2$d MB Gestione della cache Dimensione Cache attuale %1$d riquadri della mappa @@ -959,11 +850,8 @@ Gestore Offline Eliminazione della cache SQL non riuscita, vedere logcat per i dettagli Cache SQL eliminata per %1$s - Segnalazione su mappa Do il consenso a condividere i dati non cifrati del nodo tramite MQTT Abilitando questa funzione, l'utente riconosce e acconsente espressamente alla trasmissione della posizione geografica in tempo reale del suo dispositivo su protocollo MQTT senza crittografia. Questi dati di localizzazione possono essere utilizzati per scopi quali la compilazione di mappe in tempo reale, il tracking del dispositivo e le relative funzioni di telemetria. - Intervallo di segnalazione su mappa (secondi) - Il nodo invierà periodicamente un pacchetto di report mappa non cifrato al server MQTT configurato, questo include il nome id lungo e breve, posizione approssimativa, modello hardware, ruolo, versione firmware, regione LoRa, configurazione modem e nome del canale primario. Seleziona la regione da scaricare Inizia download Selezione stile mappa @@ -986,7 +874,6 @@ Inviti mesh Ascolta i beacon Cattura gli inviti annunciati dalle mesh vicine - Messaggio del beacon Massimo %1$d byte Una mesh vicina ti ha invitato a unirti Invito mesh @@ -1046,9 +933,6 @@ Messaggi µg/m³ Min - Trasmissione minima (secondi) - Distanza Intelligente - Intervallo Intelligente Tempo minimo di risveglio Preset Impostazioni moduli @@ -1058,7 +942,6 @@ MQTT Configurazione MQTT - MQTT abilitato Host non trovato Connessione non riuscita Rifiutato dal broker: %1$s @@ -1068,8 +951,6 @@ Impossibile raggiungere il broker (TCP) Timeout dopo %1$d ms Handshake TLS non riuscito: %1$s - Proxy MQTT su questo telefono - Questo telefono inoltra il traffico MQTT per il dispositivo connesso. Disattivalo per interrompere subito l'inoltro senza cambiare le impostazioni MQTT del dispositivo — utile quando il traffico MQTT satura la connessione. Riattivalo per riprendere l'inoltro. Connesso Connessione… Disconnesso @@ -1092,14 +973,12 @@ Mutato per %1$d giorni, %2$s ore Mutato per %1$s ore Non silenziato - Timeout chiusura popup (secondi) Nome Il nome non può essere vuoto. Torna Indietro Guidami Verso Informazioni Vicinato Configurazione Info Nodi Vicini - Info Nodi Vicini abilitato Rete Ricevuta URL del Nuovo Canale Nuovi messaggi sotto @@ -1112,7 +991,6 @@ Nessun dispositivo Bluetooth rilevato Nessuna sorgente di tile personalizzata trovata. Nessun dispositivo selezionato - Nessun dispositivo trovato Nessun file nel manifest. Statistiche Non Disponibili Nessun livello di mappa caricato. @@ -1171,7 +1049,6 @@ via Preferiti via MQTT NodeDB reset - Intervallo Di Trasmissione Info Nodo Nodi Nodi in questa posizione @@ -1189,6 +1066,8 @@ Non connesso Note Note + + Mesh Meshtastic utilizza le notifiche per tenerti aggiornato su nuovi messaggi e altri eventi importanti. È possibile aggiornare i permessi di notifica in qualsiasi momento dalle impostazioni. Notifiche per i canali e i messaggi diretti. @@ -1197,12 +1076,8 @@ Notifiche alla ricezione di alert/campanello Notifiche alla ricezione di messaggi Adesso - Server NTP - Numero di record - OK per MQTT Ok - Tipo OLED 24 Ore 1 Ora @@ -1220,23 +1095,13 @@ Apri le impostazioni Wi-Fi Opzioni Orientamento nord - - Output buzzer (GPIO) - Durata output (millisecondi) - Output per LED active high - LED Output (GPIO) - Output vibrazione (GPIO) Menu di overflow Sovrascrivi porta seriale della console - Ignora limite di Duty Cycle - Sovrascrivi Frequenza - Ventola PA disabilitata Autenticità dei pacchetti Bilanciato — Preferisci autenticati Consigliato. Rifiuta i tentativi di downgrade non firmati provenienti da nodi noti per firmare. Compatibile — Accetta non firmati - Autentica i pacchetti quando possibile, ma accetta traffico non firmato per la massima compatibilità. Livello di protezione Rigoroso — Richiedi l'autenticazione Attiva Rigoroso @@ -1244,7 +1109,6 @@ Mostra ed elabora solo i pacchetti della mesh autenticati crittograficamente. I nodi più vecchi e i pacchetti sovradimensionati potrebbero sparire. Attivare l'autenticazione Rigorosa? Il dispositivo connesso non supporta la verifica della firma dei pacchetti. - Modalità abbinamento Password PAX @@ -1256,7 +1120,6 @@ W:%1$d Paxcounter Configurazione Paxcounter - Paxcounter abilitato Trasmissione periodica della posizione Meshtastic ha bisogno dei permessi "Dispositivi nelle vicinanze" abilitati per trovare e connettersi ai dispositivi tramite Bluetooth. È possibile disabilitare quando non è in uso. @@ -1277,6 +1140,7 @@ %1$d secondo %1$d secondi + PM1.0 PM10 PM2.5 @@ -1284,16 +1148,11 @@ Posizione Imposta dalla posizione attuale del telefono Posizione attiva - Flag Di Posizione Posizione Pacchetto Posizione - Alimentazione Configurazione Alimentazione Metriche Alimentazione - Modulo metriche di alimentazione abilitato - Metriche di alimentazione visualizzate su schermo - Intervallo aggiornamento metriche alimentazione Alimentato ppm Posizione precisa @@ -1315,12 +1174,9 @@ Testo Principale Diffusione periodica di posizione e telemetria - Chiave Privata Fornire la posizione alla mesh Il nome del provider esiste. - Proxy to client attivato PSK - Pin PTT Chiave Pubblica Chiave Pubblica Modificata Codice QR @@ -1338,25 +1194,14 @@ Pioggia (24h) Test Distanza Configurazione Test Distanza Massima - Test distanza massima abilitato Rispondi Riavvia - - Modalità Ritrasmissione - Ritrasmettere qualsiasi messaggio osservato, se era sul nostro canale privato o da un'altra mesh con gli stessi parametri lora. - Stesso comportamento di ALL ma salta la decodifica dei pacchetti e semplicemente li ritrasmette. Disponibile solo nel ruolo Repeater. Attivando questo su qualsiasi altro ruolo, si otterrà il comportamento di ALL. - Ignora pacchetti da numeri di porta non standard come: TAK, RangeTest, PaxCounter, ecc. Ritrasmette solo pacchetti con numeri di porta standard: NodeInfo, Testo, Posizione, Telemetria e Routing. - Ignora i messaggi osservati da mesh esterne come fa LOCAL ONLY, ma in più ignora i messaggi da nodi non presenti nella lista dei nodi conosciuti. - Ignora i messaggi osservati da mesh esterne aperte o quelli che non possono essere decifrati. Ritrasmette il messaggio solo nei canali locali primario / secondario dei nodi. - Permesso solo per i ruoli SENSOR, TRACKER e TAK_TRACKER, questo inibirà tutte le ritrasmissioni, come il ruolo CLIENT_MUTE. Dispositivi di rete recenti Riconnessione… - Rosso Aggiorna Aggiorna metadati Sei sicuro di voler rigenerare la tua chiave privata?\n\nI nodi che potrebbero aver precedentemente scambiato le chiavi con questo nodo dovranno rimuovere quel nodo ed effettuare di nuovo lo scambio di chiavi per ristabilire la sicurezza nella comunicazione. Rigenera Chiavi Private - Regione Sentito %1$d relay Sentiti %1$d relay @@ -1407,31 +1252,17 @@ Ruolo Del Dispositivo Client Base Client - Tratta i pacchetti da o verso i nodi preferiti come ROUTER_LATE, e tutti gli altri pacchetti come CLIENT. - App collegata o dispositivo di messaggistica standalone. Client Nascosto - Dispositivo che trasmette solo quando necessario, per risparmiare energia o restare invisibile. Client Mute - Dispositivo che non inoltra pacchetti da altri dispositivi. Oggetti Smarriti Repeater - Nodo d'infrastruttura per estendere la copertura della rete tramite inoltro dei messaggi con overhead minimo. Non visibile nell'elenco dei nodi. Router Router Client - Combinazione di ROUTER e CLIENT. Non per dispositivi mobili. - Nodo d'infrastruttura per estendere la copertura di rete tramite inoltro dei messaggi. Visibile nell'elenco dei nodi. Router Late - Nodo dell'infrastruttura che ritrasmette sempre i pacchetti una volta ma solo dopo tutte le altre modalità, garantendo una copertura aggiuntiva per i cluster locali. Visibile nella lista dei nodi. Sensore - Dà priorità alla trasmissione di pacchetti di telemetria. TAK - Ottimizzato per la comunicazione del sistema ATAK, riduce le trasmissioni di routine. TAK Tracker - Abilita le trasmissioni automatiche TAK PLI e riduce le trasmissioni di routine. Tracker - Dà priorità alla trasmissione di pacchetti di posizione GPS. - Root topic - Encoder rotativo #1 abilitato Ho letto la <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">documentazione sui ruoli del dispositivo</a> e l'articolo del blog su <a href="http://meshtastic.org/blog/choosing-the-right-device-role">come scegliere il ruolo giusto per il dispositivo</a>. Ricevuta una conferma negativa @@ -1445,13 +1276,10 @@ Il messaggio è troppo grande per essere inviato RSSI Indicatore di forza del segnale ricevuto (Received Signal Strength Indicator), una misura utilizzata per determinare il livello di potenza ricevuto dall'antenna. Un valore RSSI più elevato indica generalmente una connessione più forte e più stabile. - server rsyslog Sat - Salva Salva & riavvia Salva - Salva .CSV nella memoria (solo ESP32) Esporta pacchetti di test di portata Scan @@ -1465,7 +1293,6 @@ Scansiona QR code contatto condiviso Scansione… Scansione… - Tieni lo schermo acceso per Scorri fino in fondo Cerca emoji... Cerca nei messaggi… @@ -1498,19 +1325,8 @@ Selezionato Tipo di mappa selezionata Invia - Invia campanella - Invia campanella con messaggio di avviso - Intervallo messaggio mittente (secondi) - Seriale - Velocità della seriale Configurazione Seriale - Console seriale - Seriale abilitata - Modalità seriale - RX - TX - Server Sessione attiva Aggiornamento richiesto Imposta l'ora @@ -1538,7 +1354,6 @@ Mostra Waypoint Spegni Nodo: %1$s - Spegnimento in mancanza di alimentazione ⚠️ Il nodo verrà SPENTO. Sarà necessario un intervento manuale per riaccenderlo. Segnale Qualità Segnale @@ -1570,7 +1385,6 @@ Usa la posizione del nodo attuale Ignora Slot - Posizione Smart SNR Rapporto segnale-rumore (Signal-to-Noise Ratio), una misura utilizzata nelle comunicazioni per quantificare il livello di un segnale desiderato rispetto al livello di rumore di fondo. In Meshtastic e in altri sistemi wireless, un SNR più elevato indica un segnale più chiaro che può migliorare l'affidabilità e la qualità della trasmissione dei dati. Umidità del Suolo @@ -1578,16 +1392,11 @@ Velocità %1$d Km/h %1$d mph - Spread Factor - SSID - Trasmissione stato (secondi) Messaggio di Stato Rimani connesso ovunque Interrompi Connessione Store & Forward Configurazione Store & Forward - Store & Forward abilitato - Subnet Operazione riuscita Durata super deep sleep Supportato @@ -1595,21 +1404,10 @@ Elimina Silenzia Riattiva l'audio - Migliora guadagno in Ricezione Impostazioni di Sistema TAK (ATAK) Configurazione TAK - Ruolo membro - Osservatore avanzato - Quartier generale - Cane (K9) - Soccorritore - Radiotelefonista - Tiratore scelto - Capo squadra - Membro della squadra - Non specificato Server TAK Abilita server TAK locale … @@ -1622,22 +1420,6 @@ ✗ Esegui In esecuzione: %1$s - Colore squadra - Blu - Marrone - Ciano - Blu scuro - Verde scuro - Verde - Magenta - Bordeaux - Arancione - Viola - Rosso - Verde acqua - Non specificato - Bianco - Giallo Telemetria Configurazione Telemetria Temperatura @@ -1646,10 +1428,8 @@ Chiaro Predefinito di sistema Ora - Fuso Orario Timeout Data e ora - TLS abilitato Attiva/disattiva posizione Trace Route @@ -1699,7 +1479,6 @@ Impossibile tradurre il messaggio Download del modello di traduzione non riuscito Il messaggio è già nella tua lingua - Trasmettere su LoRa Trasporto API @@ -1713,11 +1492,11 @@ 24H 48 Ore 2S - Trasmissione Abilitata - Potenza di Trasmissione Tipo Inserisci un messaggio Trasmissione UDP + dBm + m Predefinito di sistema Imperiale @@ -1735,9 +1514,6 @@ Riattiva selezionati Non riconosciuto Disattiva - 0 - Input Su/Giu/Selezione abilitato - Intervallo Interrogazione GPS - Intervallo di aggiornamento (secondi) Aggiornato I messaggi provenienti dalla rete mesh saranno inviati alla rete Internet pubblica attraverso il gateway configurato di qualsiasi nodo. Tempo di attività @@ -1748,13 +1524,7 @@ Template dell'URL USB Permesso USB negato. Riconnetti il dispositivo per riprovare. - - Usa formato orologio 12h Codifica compatta per cirillico - Usa I2S come buzzer - Usa modalità INPUT_PULLUP - Usa Preset - Usa buzzer PWM Utente Configurazione Utente @@ -1762,7 +1532,6 @@ Informazioni utente Stringa Utente Informazioni Utente - Username UV Lux via API via MQTT @@ -1770,8 +1539,6 @@ Visualizza sulla mappa Visualizza Release Tensione - Durata attesa Bluetooth - Accendi lo schermo al tocco o al movimento Attenzione Elimina waypoint? Modifica waypoint diff --git a/core/resources/src/commonMain/composeResources/values-ja/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ja/schema_strings.xml new file mode 100644 index 0000000000..e6aaa7f4a5 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ja/schema_strings.xml @@ -0,0 +1,280 @@ + + + + + 青 + 電流 + 緑 + LEDの状態 + 赤 + デフォルト + CODEC2 サンプルレート + CODEC 2 を有効化 + I2S データ OUT + I2S クロック + I2S データ IN + I2S ワードセレクト + PTT端子 + Bluetoothを有効 + ペアリングモード + CCWで入力イベントを生成 + CWで入力イベントを生成 + プレスで入力イベントを生成 + ロータリーエンコーダAポート用のGPIOピン + ロータリーエンコーダBポート用GPIOピン + ロータリーエンコーダプレスポート用GPIOピン + 戻る + キャンセル + なし + 選択 + ロータリーエンコーダ #1 を有効化 + ベルを送信 + 上下/選択入力を有効化 + 検出トリガーの種類 + 検出センサーを有効化 + モニターのGPIOピン + 名前 + アラートメッセージ付きのベルを送信 + 高 + INPUT_PULLUP モードを使用 + ボタン GPIO + ブザー GPIO + ダブルタップをボタンとして使用 + 加速度センサー搭載デバイスで本体をダブルタップすると、ボタンのプッシュと同じ動作として扱います。 + LED ハートビート + ノード情報のブロードキャスト間隔 + 再送信モード + すべて + すべてをスキップ + ALLと同じ動作ですが、パケットのデコードをスキップして単純に再ブロードキャストします。 リピーターロールでのみ使用できます。他のロールに設定すると、ALLの動作になります。 + コアポート番号のみ + 既知のみ + ローカルのみ + 開いている外部メッシュや復号できないメッシュからのメッセージを無視。 ノードのローカルプライマリー/セカンダリーチャンネルでのみメッセージを再ブロードキャスト。 + なし + SENSOR、TRACKER、およびTAK_TRACKERロールでのみ許可。CLIENT_MUTEロールとは異なり、すべての再ブロードキャストを禁止。 + デバイスの役割 + クライアント + クライアント・ベース + クライアント・非表示 + ステルスまたは電力節約のため、必要に応じてのみブロードキャストするデバイス。 + クライアント・ミュート + このデバイスは他のデバイスからのパケットを転送しません。 + 紛失モード + デバイスを見つけやすくするために、デバイス自身の位置情報をメッセージ形式で定期的にデフォルトのチャンネルにブロードキャストします。 + リピーター + ルーター + ルータークライアント + ルーター・レイト + センサー + テレメトリパケットを優先してブロードキャストします。 + TAK + ATAKシステムとの通信に最適化し、定期的なブロードキャストを削減します。 + TAK Tracker + TAK PLIの自動ブロードキャストを有効にし、ルーチンブロードキャストを削減します。 + トラッカー + GPSの位置情報パケットを優先してブロードキャストします。 + タイムゾーン + カルーセルの間隔 + 指定した間隔に基づき、画面上でカルーセルのように自動的に次のページに切り替わります。 + 常に北を上にする + 円外側の画面上のコンパス方位は、常に北を指します。 + コンパスの向き + 表示モード + デフォルト画面レイアウトを上書きします。 + メートル法 + 画面反転 + 画面を上下に反転させる。 + 太字の見出し + 画面の見出しテキストを太字にします。 + OLED タイプ + OLED 画面の自動検出を上書きします。 + 画面オンの時間 + ユーザーボタンが押された後、またはメッセージが受信された後、画面がオンになっている期間。 + 表示単位 + デバイスの画面に表示されている単位。 + 12時間の時計形式を使用 + 有効にすると、デバイスは時刻を12時間形式で表示します。 + タップまたは動作で起動 + お使いのデバイスに加速度センサーがあることが必要です。 + LED出力 アクティブハイ + LED + ブザー + バイブレーション + LED + ブザー + バイブレーション + 外部通知を有効化 + LED出力 (GPIO) + ブザー出力(GPIO) + バイブレーション出力 (GPIO) + I2Sをブザーとして使用 + PWMブザーを使用 + 帯域 + 周波数スロット + ノードの動作周波数は、リージョン、モデムプリセット、およびこのフィールドに基づいて計算されます。0 の場合、スロットはプライマリーチャンネル名に基づいて自動的に計算され、デフォルトのパブリックスロットから変更されます。プライベートなプライマリーチャンネルとパブリックなセカンダリーチャンネルを設定している場合は、パブリックのデフォルトスロットに戻してください。 + 符号化レート + MQTT への送信を許可 + ホップ数 + 最大ホップ数を設定し、初期値は3ホップです。ホップ数を増やすと輻輳も増加するため、控えめに運用しましょう。0ホップのブロードキャストメッセージはACKを受信しなくなります。 + MQTT を無視 + プリセット + Lite・高速 + Lite・低速 + 長距離・高速 + 長距離・中速 + 長距離・低速 + 長距離・ターボ + 中距離・高速 + 中距離・低速 + 中距離・ターボ + 狭帯域・高速 + 狭帯域・低速 + 短距離・高速 + 短距離・低速 + 短距離・ターボ + 極短距離・高速 + 極短距離・低速 + 超長距離・低速 + デューティサイクルを上書き + 周波数の上書き + PAファン無効 + リージョン + 無線デバイスを使用される地域を指定してくだい。 + 拡散率 + RX ブーストゲイン + 送信を有効化 + 送信出力 + プリセットを使用 + 前線観測員 (FO) + 衛生兵 + スナイパー + チームリーダー + チームメンバー + メッセージ + アドレス + MQTTを有効化 + 暗号化の有効化 + マップ報告 + パスワード + クライアントへのプロキシの有効化 + ルート トピック + ユーザー名 + 近隣ノード情報を有効化 + LoRaで送信 + 近隣ノード情報(NeighborInfo)をMQTTやPhoneAPIへ送信することに加えて、LoRa無線経由でも送信すべきかどうかを設定します。デフォルトの名前とキーが設定されたチャンネルでは利用できません。 + GPS ポーリング間隔 + IPv4 モード + UDP 経由でローカルネットワーク上のパケットのブロードキャスト通信を有効にする。 + イーサネット有効 + イーサネットを有効にすると、アプリへの Bluetooth 接続が無効になります。TCP でのノード接続は Apple デバイスでは利用できません。 + DNS + ゲートウェイ + IP + サブネット + NTPサーバー + なし + rsyslogサーバー + WiFi を有効化 + WiFi を有効にすると、アプリへの Bluetooth 接続が無効になります。 + パスワード + SSID + BLE RSSI閾値(デフォルトは -80) + Paxcounter を有効化 + GPS ポーリング間隔 + スマート距離 + スマート位置情報ブロードキャストの対象とする最小の移動距離(メートル)。 + スマート間隔 + 固定位置 + GPS EN GPIO + GPS モード(物理ハードウェア) + GPS ポーリング間隔 + GPS 位置情報を取得する頻度(<10 秒で GPS をオンに保ちます)。 + 無効 + 有効 + ブロードキャスト間隔 + ノードが位置情報をブロードキャストせずに経過し得る最大間隔。 + スマート位置情報 + 位置情報フラグ + 位置情報メッセージを構成する際に含めるオプションのフィールド。含めるフィールドが多いほどメッセージは大きくなり、エアタイムが長くなってパケット損失のリスクが高まります。 + 標高 + タイムスタンプ + GPS RX GPIO + GPS TX GPIO + ADC倍率のオーバーライド比 + 省電力モードを有効化 + 可能な限りすべてをスリープさせます。トラッカーおよびセンサーの役割の場合は、LoRa 無線機も対象になります。スマートフォンアプリでデバイスを使用したい場合や、ユーザーボタンのないデバイスを使用している場合は、この設定を使用しないでください。 + 電源喪失時にシャットダウン + Bluetooth 待機時間 + レンジテストを有効化 + ストレージにCSVファイルを保存(ESP32のみ) + 管理者キー + このノードに管理メッセージを送信することを許可された公開鍵。 + デバッグログAPIを有効化 + シリアル経由でライブデバッグログを出力し、位置情報を秘匿したデバイスログを Bluetooth 経由で表示・エクスポートします。 + 管理モード + デバイスはメッシュ管理者によって管理されており、ユーザーはデバイス設定にアクセスできません。 + 秘密鍵 + 公開鍵 + シリアルコンソール + Stream API 経由のシリアルコンソール。 + シリアルボーレイト + シリアルボーレイト + Echoを有効化 + シリアル通信を有効化 + シリアルモード + RX + デフォルト + デフォルト + タイムアウト + TX + ストアアンドフォワード有効 + ハートビート + リクエスト可能な最大の履歴件数 + リクエスト可能な履歴の期間 (分) + サーバー + サーバーの最大保管レコード数 (デフォルト 約11,000レコード) + 役割 + 青 + 茶色 + シアン + 紺色 + ダークグリーン + 緑 + マゼンタ + 栗色 + オレンジ + 紫色 + 赤 + ティール + 白色 + 黄色 + 空気品質測定モジュールを有効化 + 空気品質メトリクスの更新間隔 + デバイステレメトリを送信 + デバイステレメトリモジュールの有効/無効を切り替えて、メトリクスをメッシュに送信します。これらは公称値です。混雑したメッシュでは、オンラインノード数に応じて間隔が自動的に長くなります。 + デバイスメトリクスの更新間隔 + 環境メトリクスは華氏を使用 + 環境メトリクスモジュールを有効化 + 環境メトリクスを画面上で有効化 + 環境メトリクスの更新間隔 + 電源メトリクスモジュール有効 + 電源メトリクスを画面上で有効化 + 電源メトリクスの更新間隔 + 不明なパケットのしきい値 + diff --git a/core/resources/src/commonMain/composeResources/values-ja/strings.xml b/core/resources/src/commonMain/composeResources/values-ja/strings.xml index 264ade2b58..cd5378bcba 100644 --- a/core/resources/src/commonMain/composeResources/values-ja/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ja/strings.xml @@ -17,17 +17,21 @@ --> + 湿度 %1$s:%2$s %1$s からのメッセージ:%2$s %1$s 先 お気に入り - %1$d ホップ先 + + %1$d ホップ先 + 最終受信 %1$s オフライン オンライン 役割 %1$s 信号 %1$s ホップ %1$d:%2$d ノード + 温度 概要 同意 謝辞 @@ -44,7 +48,6 @@ メッセージを翻訳 アクション ADC倍率のオーバーライド - ADC倍率のオーバーライド比 追加 プライベートメモを追加… @@ -57,7 +60,6 @@ 手動でデバイスを追加… ネットワークレイヤーを追加 アドレス - 管理者キー 管理者キー 管理 詳細設定 @@ -67,24 +69,14 @@ 空気質 空気品質アイコン 空気品質メトリクスのログ - 空気品質測定モジュールを有効化 - 空気品質メトリクスの更新間隔 過去1時間以内に送信に使用された通信時間の割合。 AirUtil - - ブザー - LED アラートベル! - バイブレーション - ブザー - LED - バイブレーション すべて 入力ソースを許可 未定義のピンへのアクセスを許可 高度 標高 - 常に北を上にする 環境照明 環境照明設定 Android アプリの改善に役立てるため、分析データを収集します(ご協力ありがとうございます)。ユーザー行動に関する匿名化された情報を受け取ります。これにはクラッシュレポートやアプリ内で使用された画面などが含まれます。 @@ -131,20 +123,15 @@ 公開鍵と秘密鍵を、このデバイスの安全な暗号化ストレージに保存します。 バックアップと復元 不良 - - 帯域 気圧 バッテリー バッテリー INA_2XX I2C アドレス Bluetooth デバイス - BLE RSSI閾値(デフォルトは -80) - 青 Bluetooth 利用可能な Bluetooth デバイス Bluetooth 設定 Bluetooth がオフです。近くのデバイスをスキャンするにはオンにしてください。 - Bluetoothを有効 設定 デバイスの設定とチャンネルをワイヤレスで管理します。 探索 @@ -156,14 +143,10 @@ Bluetooth スキャンの上限に達しました。%1$d 秒後にもう一度お試しください。 - 太字の見出し ペアリングに失敗しました。付近のデバイスの権限を許可して、もう一度お試しください。 ペアリングが完了しませんでした。もう一度ペアリングしてください。 設定 - ブロードキャスト間隔 ビジーフロア - ボタン GPIO - ブザー GPIO 計算中… コールサイン カメラの権限 @@ -176,7 +159,6 @@ 定型メッセージを有効化 デバイスが未接続のため、チャンネルが変更できませんでした。もう一度やり直してください。 このデバイスでシャットダウンはサポートされていません - カルーセルの間隔 現在のチャンネルの使用率。正常な送信(TX)、正常な受信(RX)、および不正な受信 (ノイズ)を含みます。 Ch @@ -190,7 +172,6 @@ チャンネル 8 チャンネル機能 このチャンネルURLは無効なため使用できません - チャンネル チャンネル名 チャンネル URL ChUtil @@ -231,9 +212,6 @@ CO₂ CO₂ 湿度 CO₂ 温度 - CODEC 2 を有効化 - CODEC2 サンプルレート - 符号化レート チャートを折りたたむ 折りたたみ済み 携帯電話の電波がなくても、友人やコミュニティとオフグリッドで通信できます。 @@ -246,47 +224,22 @@ 距離と方位を表示するには位置情報の権限が必要です。 このデバイスにはコンパスセンサーがありません。方位は利用できません。 ノースアップ表示 - コンパスの向き コンパス 推定範囲:\u00b1%1$s(\u00b1%2$s) 推定範囲:精度不明 危殆化した鍵が検出されました。再生成するには OK を選択してください。 - 加速度センサー搭載デバイスで本体をダブルタップすると、ボタンのプッシュと同じ動作として扱います。 デバイスの点滅するLEDを制御します。ほとんどのデバイスでは、最大4つあるLEDのうちの1つを制御します。充電用LEDとGPS用LEDは制御できません。 - 近隣ノード情報(NeighborInfo)をMQTTやPhoneAPIへ送信することに加えて、LoRa無線経由でも送信すべきかどうかを設定します。デフォルトの名前とキーが設定されたチャンネルでは利用できません。 ユーザーボタンをトリプルクリックすると、プライマリチャンネルに位置情報を送信します。 デバイスの画面とログ上の日付のタイムゾーン。 デバイスのタイムゾーンを使用 - 指定した間隔に基づき、画面上でカルーセルのように自動的に次のページに切り替わります。 - 円外側の画面上のコンパス方位は、常に北を指します。 - デフォルト画面レイアウトを上書きします。 - 画面を上下に反転させる。 - 画面の見出しテキストを太字にします。 - OLED 画面の自動検出を上書きします。 - ユーザーボタンが押された後、またはメッセージが受信された後、画面がオンになっている期間。 - デバイスの画面に表示されている単位。 - お使いのデバイスに加速度センサーがあることが必要です。 - ノードの動作周波数は、リージョン、モデムプリセット、およびこのフィールドに基づいて計算されます。0 の場合、スロットはプライマリーチャンネル名に基づいて自動的に計算され、デフォルトのパブリックスロットから変更されます。プライベートなプライマリーチャンネルとパブリックなセカンダリーチャンネルを設定している場合は、パブリックのデフォルトスロットに戻してください。 - 最大ホップ数を設定し、初期値は3ホップです。ホップ数を増やすと輻輳も増加するため、控えめに運用しましょう。0ホップのブロードキャストメッセージはACKを受信しなくなります。 このリージョンのプリセットは、免許を持つ(アマチュア無線)運用者専用です。選択するには、ユーザー設定で「免許制アマチュア無線(Ham)」を有効にしてください。 使用可能なモデムプリセット、デフォルトはロングファーストです。 無線デバイスを使用される地域を指定してくだい。 - イーサネットを有効にすると、アプリへの Bluetooth 接続が無効になります。TCP でのノード接続は Apple デバイスでは利用できません。 UDP 経由でローカルネットワーク上のパケットのブロードキャスト通信を有効にする。 - ノードが位置情報をブロードキャストせずに経過し得る最大間隔。 - スマート位置情報ブロードキャストの対象とする最小の移動距離(メートル)。 - 最小距離を満たした場合に位置情報の更新を送信する最短間隔。 - 位置情報メッセージを構成する際に含めるオプションのフィールド。含めるフィールドが多いほどメッセージは大きくなり、エアタイムが長くなってパケット損失のリスクが高まります。 - GPS 位置情報を取得する頻度(<10 秒で GPS をオンに保ちます)。 - 可能な限りすべてをスリープさせます。トラッカーおよびセンサーの役割の場合は、LoRa 無線機も対象になります。スマートフォンアプリでデバイスを使用したい場合や、ユーザーボタンのないデバイスを使用している場合は、この設定を使用しないでください。 ノードが再起動しており、しばらく接続できなくなります。 - このノードに管理メッセージを送信することを許可された公開鍵。 - シリアル経由でライブデバッグログを出力し、位置情報を秘匿したデバイスログを Bluetooth 経由で表示・エクスポートします。 - デバイスはメッシュ管理者によって管理されており、ユーザーはデバイス設定にアクセスできません。 リモートデバイスとの共有鍵を作成するために使用されます。 秘密鍵から生成され、メッシュ上の他のノードに送信されて、共有秘密鍵を計算できるようにします。 - Stream API 経由のシリアルコンソール。 設定 Bluetooth の権限を設定 重要なアラートを設定 @@ -329,7 +282,6 @@ フィルタを含む プリセットフィルタ フィルタ - デバッグログAPIを有効化 表示するアプリログがありません 更新 ログのエクスポート @@ -377,8 +329,6 @@ 詳細 検出センサー 検出センサ設定 - 検出センサーを有効化 - 検出トリガーの種類 デバイス デバイス設定 @@ -392,17 +342,15 @@ デバイスメトリクス %1$s %1$s:%2$s%% - デバイスメトリクスの更新間隔 %1$s:%2$s V デバイスはスリープ状態です デバイスストレージと UI(読み取り専用) - デバイステレメトリを送信 - デバイステレメトリモジュールの有効/無効を切り替えて、メトリクスをメッシュに送信します。これらは公称値です。混雑したメッシュでは、オンラインノード数に応じて間隔が自動的に長くなります。 テーマ:%1$s、言語:%2$s 露点 ダイレクトメッセージ ダイレクトメッセージの鍵 ダイレクトメッセージ + 無効 破棄 切断 切断 @@ -479,22 +427,19 @@ スキャン停止 AI 分析は利用できません 残り %1$s - %1$d 個のユニークノード + + %1$d 個のユニークノード + マップを表示 空きディスク %1$d - 表示 デバイスの表示 - 表示モード - 有効にすると、デバイスは時刻を12時間形式で表示します。 - 表示単位 距離 距離フィルター スマートフォンからの距離に基づいて、ノードリストとメッシュマップをフィルタリングします。 距離の計測 位置情報を持つ他の Meshtastic ノードとスマートフォンの間の距離を表示します。 - DNS 検索をクリア システムAI,Gemini,アシスタント,機能,自動化,音声,system ai,gemini,assistant,functions,automation,voice @@ -535,6 +480,7 @@ MQTT ノードメトリクス ノード + 通知 はじめに 設定:モジュールと管理 設定:無線とユーザー @@ -546,38 +492,28 @@ ホーム画面ウィジェット 完了 このデバイスでは今後表示しない - ダブルタップをボタンとして使用 パブリックなインターネットゲートウェイからのメッセージが、ローカルメッシュに転送されます。ゼロホップポリシーにより、デフォルトの MQTT サーバーからのトラフィックはこのデバイスより先には伝播しません。 ダウンロード 公開鍵の重複を検出 + 動的 遠隔地での安全で信頼性の高い通信のために、プライベートメッシュネットワークを簡単にセットアップできます。 - Echoを有効化 編集 ネットワークタイルソースを編集 8時間 - 省電力モードを有効化 有効 - - 暗号化の有効化 公開鍵が一致しません 公開鍵が記録されている鍵と一致しません。ノードを削除して鍵を再交換させることもできますが、これはより深刻なセキュリティ上の問題を示している可能性があります。鍵の変更が工場出荷時リセットやその他の意図的な操作によるものかどうかを確認するため、別の信頼できる手段でユーザーに連絡してください。 公開鍵暗号化 環境メトリクス - 環境 - 環境メトリクスモジュールを有効化 - 環境メトリクスを画面上で有効化 - 環境メトリクスの更新間隔 - 環境メトリクスは華氏を使用 エラー デューティサイクル制限に達しました。現在メッセージを送信できません。しばらくしてからもう一度お試しください。 繰り返し試行しましたが、安定した接続を確立できませんでした。ノードを選択し直して再試行してください。 接続して管理 リモートセッションを確立しています… イーサネットオプション - イーサネット有効 イーサネット IP: イベントテーマを使用 位置交換 @@ -591,7 +527,6 @@ TAK データパッケージをエクスポート 外部通知 外部通知設定 - 外部通知を有効化 出荷時にリセット 普通 Meshtastic %1$s @@ -602,10 +537,6 @@ 利用可能なファイル(%1$d): 単語または regex:パターンを追加 - フィルタを無効にする - フィルタリングを有効化 - フィルタリングを有効化 - フィルター単語を含むメッセージを非表示にします フィルター済み %1$d 件を非表示 絞り込み フィルター済み @@ -711,9 +642,7 @@ デバイスが OTA モードで再起動するのを待っています… デバイスの再接続を待っています… ファームウェアバージョン:%1$s - 固定PIN 固定位置 - 画面反転 詳細については、プライバシーポリシーをご覧ください。 太字 @@ -724,13 +653,7 @@ 空きメモリ 利用可能なシステムメモリ(バイト単位) 周波数 - 周波数スロット - 名前 ガス抵抗 - ゲートウェイ - CCWで入力イベントを生成 - CWで入力イベントを生成 - プレスで入力イベントを生成 QRコード生成 ジオフェンス @@ -754,31 +677,17 @@ マップ上でエリアを設定 開始する 良 - GPIO GPIO端子 - ロータリーエンコーダAポート用のGPIOピン - ロータリーエンコーダBポート用GPIOピン - ロータリーエンコーダプレスポート用GPIOピン - モニターのGPIOピン - GPS EN GPIO - GPS モード(物理ハードウェア) - GPS RX GPIO - GPS TX GPIO 権限を許可 - 緑 ハードウェア ハードウェアのモデル 方角 - ハートビート ヘルプとドキュメント レイヤーを非表示 パスワードを非表示 - リクエスト可能な最大の履歴件数 - リクエスト可能な履歴の期間 (分) この期間に受信したノードはありません ホップごとのノード数 - ホップ数 ホップ数 ホスト ホストのメトリクス @@ -786,18 +695,12 @@ 同意します。 上記を読んで理解しています。MQTTを通じて自分のノードデータの暗号化されていない送信に自発的に同意します はい、了承します。 - I2S クロック - I2S データ IN - I2S データ OUT - I2S ワードセレクト IAQ (屋内空気品質) 相対スケールIAQ値は、ボッシュBME680によって測定されます。 値の範囲は 0-500。 アイコンの意味 - 無視 '%1$s'を無視リストに追加しますか? 無視リスト (ノード番号を登録) - MQTT を無視 '%1$s'を無視リストから削除しますか? 設定をインポート @@ -823,7 +726,6 @@ IP IPアドレス: ポート: - IPv4 モード JSON出力の有効化 %1$s @@ -842,8 +744,6 @@ 緯度 詳細 - LED ハートビート - LEDの状態 レガシー管理チャンネル %1$d 個のライブラリ @@ -929,11 +829,9 @@ ルクス カスタムタイルソースを管理 マップレイヤーを管理 - 管理モード 手動での位置情報のリクエストが必要 メッシュマップ - キャッシュ容量: %1$d MB\nキャッシュ使用量: %2$d MB キャッシュの管理 現在のキャッシュサイズ %1$d タイル @@ -951,11 +849,8 @@ オフライン地図の管理 SQL キャッシュの削除に失敗しました。詳細は logcat を参照してください。 %1$sがSQLキャッシュから削除されました。 - マップレポート MQTT 経由で暗号化されていないノードデータの共有に同意する この機能を有効にすると、デバイスのリアルタイムの位置情報が暗号化されていないMQTTプロトコルで送信されることに同意したと見なされます。この位置データは、ライブマップへの表示、デバイスの追跡、および関連するテレメトリ機能といった目的で利用される可能性があります。 - マップレポートの間隔 (秒) - お使いのノードは、設定済みのMQTTサーバーに対し、暗号化されていないマップレポートのパケットを定期的に送信します。これには、ID、正式名称と短縮名、概算位置、ハードウェアのモデル、ロール、ファームウェアのバージョン、LoRaリージョン、モデムのプリセット、そしてプライマリチャンネル名が含まれています。 指定範囲の地図タイルをダウンロード ダウンロード開始 マップスタイルの選択 @@ -978,7 +873,6 @@ メッシュへの招待 ビーコンを受信 近くのメッシュが発信する招待を受け取ります - ビーコンメッセージ 最大 %1$d バイト 近くのメッシュから参加への招待が届きました メッシュへの招待 @@ -1038,9 +932,6 @@ メッセージ µg/m³ 最小 - 最小ブロードキャスト間隔(秒) - スマート距離 - スマート間隔 最小起動時間 プリセット 追加機能の設定 @@ -1050,7 +941,6 @@ MQTT MQTT設定 - MQTTを有効化 MQTT:接続が失われました MQTT:接続が拒否されました(認証情報を確認してください) MQTT プロキシに失敗しました:%1$s @@ -1064,8 +954,6 @@ サーバーに到達できません(TCP) %1$d ms 後にタイムアウトしました TLS ハンドシェイクに失敗しました:%1$s - このスマートフォンの MQTT プロキシ - このスマートフォンが、接続中のデバイスの MQTT トラフィックを中継します。オフにすると、デバイスの MQTT 設定を変更せずに中継をすぐに停止できます。MQTT トラフィックが接続を圧迫している場合に便利です。再びオンにすると中継を再開します。 接続済 接続しています… 切断 @@ -1088,14 +976,12 @@ %1$d 日 %2$s 時間ミュート中 %1$s 時間ミュート中 ミュートなし - 繰り返し通知間隔(秒) 名前 名前を空にすることはできません。 前に戻る ナビゲートする 隣接ノード情報 近隣ノード情報 (Neighbor Info) の設定 - 近隣ノード情報を有効化 ネットワーク 新しいチャンネルURLを受信しました 下に新着メッセージ @@ -1108,7 +994,6 @@ Bluetooth デバイスが見つかりません カスタムタイルソースが見つかりません。 デバイスが選択されていません - デバイスが見つかりません ファイルがありません。 統計がありません マップレイヤーが読み込まれていません。 @@ -1167,7 +1052,6 @@ お気に入り経由 MQTT経由 NodeDBをリセット - ノード情報のブロードキャスト間隔 ノード この場所のノード @@ -1185,6 +1069,8 @@ 接続されていません メモ メモ + + メッシュ Meshtastic は、新着メッセージやその他の重要なイベントをお知らせするために通知を使用します。通知の権限は、設定からいつでも更新できます。 チャンネルメッセージとダイレクトメッセージの通知。 @@ -1193,12 +1079,8 @@ アラートベル受信時の通知 メッセージ受信時の通知 今 - NTPサーバー - サーバーの最大保管レコード数 (デフォルト 約11,000レコード) - MQTT への送信を許可 OK - OLED タイプ 24時間 1時間 @@ -1216,23 +1098,13 @@ Wi-Fi 設定を開く オプション 北を上に向ける - - ブザー出力(GPIO) - 出力時間 (ミリ秒) - LED出力 アクティブハイ - LED出力 (GPIO) - バイブレーション出力 (GPIO) オーバーフローメニュー コンソールのシリアルポートを上書き - デューティサイクルを上書き - 周波数の上書き - PAファン無効 パケットの真正性 バランス:認証済みを優先 推奨。署名することが分かっているノードからの、署名なしへのダウングレードを拒否します。 互換:署名なしを許可 - 可能な場合はパケットを認証しますが、互換性を最大にするため署名なしの通信も受け入れます。 保護レベル 厳格:認証を必須にする 厳格を有効にする @@ -1240,7 +1112,6 @@ 暗号的に認証されたメッシュパケットのみを表示・処理します。古いノードや大きすぎるパケットは表示されなくなることがあります。 厳格な認証を有効にしますか? この接続中のデバイスは、パケット署名の検証に対応していません。 - ペアリングモード パスワード PAX @@ -1252,7 +1123,6 @@ W:%1$d Paxcounter Paxcounter 設定 - Paxcounter を有効化 定期的な位置情報ブロードキャスト Meshtastic が Bluetooth でデバイスを検出して接続するには、「付近のデバイス」の権限を有効にする必要があります。使用しないときは無効にできます。 @@ -1270,6 +1140,7 @@ %1$d 秒 + PM1.0 PM10 PM2.5 @@ -1277,16 +1148,11 @@ 位置 現在のスマートフォンの位置から設定 位置情報共有の有効化 - 位置情報フラグ 位置 位置情報パケット - 電源 電源設定 電源メトリクス - 電源メトリクスモジュール有効 - 電源メトリクスを画面上で有効化 - 電源メトリクスの更新間隔 給電 ppm 正確な位置情報 @@ -1308,12 +1174,9 @@ テキスト プライマリ 定期的な位置情報とテレメトリのブロードキャスト - 秘密鍵 メッシュネットワークにスマホの位置情報を提供 プロバイダー名がすでに存在します。 - クライアントへのプロキシの有効化 PSK - PTT端子 公開鍵 公開鍵が変更されました QRコード @@ -1331,25 +1194,14 @@ 降水量(24時間) レンジテスト レンジテスト設定 - レンジテストを有効化 リアクション 再起動 - - 再送信モード - 受信メッセージが、参加しているプライベートチャンネル上のもの、または同じLoRaパラメータを持つ別のメッシュからのものであれば再ブロードキャストします。 - ALLと同じ動作ですが、パケットのデコードをスキップして単純に再ブロードキャストします。 リピーターロールでのみ使用できます。他のロールに設定すると、ALLの動作になります。 - TAK、RangeTest、PaxCounterなどの非標準ポート番号からのパケットを無視。NodeInfo、Text、Position、Telemetry、Routingなどの標準ポート番号を持つパケットのみを再ブロードキャスト。 - LOCAL ONLYのような外部メッシュからのメッセージを無視します。 さらに一歩進んで既知のノードリストにないノードからのメッセージを無視します。 - 開いている外部メッシュや復号できないメッシュからのメッセージを無視。 ノードのローカルプライマリー/セカンダリーチャンネルでのみメッセージを再ブロードキャスト。 - SENSOR、TRACKER、およびTAK_TRACKERロールでのみ許可。CLIENT_MUTEロールとは異なり、すべての再ブロードキャストを禁止。 最近のネットワークデバイス 再接続しています… - 赤 更新 メタデータを更新 秘密鍵を再生成してもよろしいですか?\n\n以前にこのノードと鍵を交換したことのあるノードは、安全な通信を再開するために、そのノードを削除して鍵を再交換する必要があります。 秘密鍵を再生成 - リージョン %1$d 件のリレーを受信 @@ -1399,31 +1251,17 @@ デバイスの役割 クライアント クライアント・ベース - お気に入りのノードとの間のパケットを ROUTER_LATE として扱い、その他のすべてのパケットを CLIENT として扱います。 - アプリに接続されているか、スタンドアロンのメッセージングデバイスです。 クライアント・非表示 - ステルスまたは電力節約のため、必要に応じてのみブロードキャストするデバイス。 クライアント・ミュート - このデバイスは他のデバイスからのパケットを転送しません。 紛失モード リピーター - 最小限のオーバーヘッドでメッセージを中継することでネットワークの通信範囲を拡大するためのインフラストラクチャノード。ノードリストには表示されません。 ルーター ルータークライアント - ROUTERとCLIENTの組み合わせ。モバイルデバイス向けではありません。 - メッセージを中継することでネットワークの通信範囲を拡大するためのインフラストラクチャノード。ノードリストに表示されます。 ルーター・レイト - 周辺クラスターの通信範囲を拡大させるインフラストラクチャノード。他のすべてのノードが通信し終わった後で、必ずパケットを1回だけ再ブロードキャストする。ノードリストに表示される。 センサー - テレメトリパケットを優先してブロードキャストします。 TAK - ATAKシステムとの通信に最適化し、定期的なブロードキャストを削減します。 TAK Tracker - TAK PLIの自動ブロードキャストを有効にし、ルーチンブロードキャストを削減します。 トラッカー - GPSの位置情報パケットを優先してブロードキャストします。 - ルート トピック - ロータリーエンコーダ #1 を有効化 <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">デバイスの役割に関するドキュメント</a>と、<a href="http://meshtastic.org/blog/choosing-the-right-device-role">適切なデバイスの役割の選び方</a>に関するブログ記事を読みました。 相手が正常に受信できませんでした @@ -1437,13 +1275,10 @@ メッセージが大きすぎて送信できません RSSI 受信信号強度インジケーター(RSSI)は、アンテナで受信している電力レベルを測定するための指標です。一般的にRSSI値が高いほど、より強力で安定した接続を示します。 - rsyslogサーバー GPS衛星 - 保存 保存して再起動 保存 - ストレージにCSVファイルを保存(ESP32のみ) レンジテストのパケットをエクスポート スキャン @@ -1457,7 +1292,6 @@ 共有連絡先のQRコードをスキャン スキャンしています… スキャンしています… - 画面オンの時間 一番下までスクロール 絵文字を検索… メッセージを検索… @@ -1490,19 +1324,8 @@ 選択済み 選択中のマップタイプ 送信 - ベルを送信 - アラートメッセージ付きのベルを送信 - 送信者のメッセージ間隔 (秒) - シリアル - シリアルボーレイト シリアル設定 - シリアルコンソール - シリアル通信を有効化 - シリアルモード - RX - TX - サーバー セッション有効 更新が必要 時刻を設定 @@ -1531,7 +1354,6 @@ ウェイポイントを表示 シャットダウン ノード:%1$s - 電源喪失時にシャットダウン ⚠️ ノードをシャットダウンします。再びオンにするには物理的な操作が必要になります。 信号 信号品質 @@ -1563,7 +1385,6 @@ 現在のノードの位置を使用 スキップ スロット - スマート位置情報 SN比 信号対ノイズ比(SN比)は、通信において、目的の信号のレベルを背景ノイズのレベルに対して定量化するために使用される尺度です。Meshtasticや他の無線システムでは、SN比が高いほど信号が鮮明であることを示し、データ伝送の信頼性と品質を向上させることができます。 土壌水分 @@ -1571,16 +1392,11 @@ 速度 %1$d km/h %1$d mph - 拡散率 - SSID - 状態のブロードキャスト間隔 (秒) ステータスメッセージ どこでもつながる 接続を停止 ストアアンドフォワード ストアアンドフォワード設定 - ストアアンドフォワード有効 - サブネット 成功 スーパーディープスリープの時間 対応済み @@ -1588,21 +1404,10 @@ 削除 ミュート ミュート解除 - RX ブーストゲイン システム設定 TAK (ATAK) TAK 設定 - メンバーロール - 前線観測員 (FO) - 本部 - イッヌ (K9) - 衛生兵 - 無線通信手 - スナイパー - チームリーダー - チームメンバー - 未指定 TAK サーバー ローカル TAK サーバーを有効化 … @@ -1615,22 +1420,6 @@ ✗ 実行 実行中:%1$s - チームカラー - 青 - 茶色 - シアン - 紺色 - ダークグリーン - 緑 - マゼンタ - 栗色 - オレンジ - 紫色 - 赤 - ティール - 未指定 - 白色 - 黄色 テレメトリ テレメトリ設定 温度 @@ -1639,10 +1428,8 @@ ライト システムのデフォルト 時刻 - タイムゾーン タイムアウト タイムスタンプ - TLS の有効化 自分の位置を切り替え トレースルート @@ -1691,7 +1478,6 @@ メッセージを翻訳できません 翻訳モデルのダウンロードに失敗しました メッセージはすでにあなたの言語です - LoRaで送信 接続方式 API @@ -1705,8 +1491,6 @@ 24時間 48時間 2週間 - 送信を有効化 - 送信出力 種別 メッセージを入力 UDP ブロードキャスト @@ -1726,9 +1510,6 @@ 選択項目のミュートを解除 不明 未設定 - 0 - 上下/選択入力を有効化 - GPS ポーリング間隔 - 更新間隔 (秒) 更新済み メッシュからのメッセージは、いずれかのノードに設定されたゲートウェイを通じてパブリックなインターネットに送信されます。 連続稼働時間 @@ -1739,13 +1520,7 @@ URL テンプレート USB USB の権限が拒否されました。デバイスを再接続してもう一度お試しください。 - - 12時間の時計形式を使用 キリル文字向けのコンパクトエンコーディング - I2Sをブザーとして使用 - INPUT_PULLUP モードを使用 - プリセットを使用 - PWMブザーを使用 ユーザー ユーザー設定 @@ -1753,7 +1528,6 @@ ユーザー情報 ユーザー文字列 ユーザー情報 - ユーザー名 UV ルクス API 経由 MQTT経由 @@ -1761,8 +1535,6 @@ マップで表示 リリースを表示 電圧 - Bluetooth 待機時間 - タップまたは動作で起動 警告 ウェイポイントを削除しますか? ウェイポイントを編集 diff --git a/core/resources/src/commonMain/composeResources/values-ko/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ko/schema_strings.xml new file mode 100644 index 0000000000..e8c9c71bf5 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ko/schema_strings.xml @@ -0,0 +1,156 @@ + + + + + 파랑 + 전류 + 초록 + LED 상태 + 빨강 + 기본값 + CODEC2 샘플 레이트 + CODEC2 활성화 + I2S 데이터 out + I2S 시간 + I2S 데이터 in + I2S 단어 선택 + PTT 핀 + 블루투스 활성화 + 페어링 모드 + 반시계방향 동작 + 시계방향 동작 + 누름 동작 + 로터리 엔코더 A포트 용 GPIO 핀 + 로터리 엔코더 B포트 용 GPIO 핀 + 로터리 엔코더 누름 포트 용 GPIO 핀 + 취소 + 없음 + 로터리 엔코더 #1 활성화 + 벨 전송 + 업/다운/선택 입력 활성화 + 디텍션 트리거 타입 + 감지 센서 활성화 + 상태 모니터링 GPIO 핀 + 식별 이름 + 알람 메시지와 벨 전송 + INPUT_PULLUP 모드 사용 + 세 번 클릭 끄기 + 가속도계가 있는 장치를 두 번 탭하여 사용자 버튼과 동일한 동작. + 노드 정보 발송 주기 + 중계 모드 + All + ALL 역할과 동일하게 동작하지만, 패킷 디코딩을 건너뛰고 단순히 재전송만 수행합니다. Repeater 일때 설정가능. 다른 Role에서는 ALL로 동작. + 핵심 포트 번호만 허용 + 오픈되어 있거나 해독할 수 없는 외부 메시에서 관찰된 메시지를 무시합니다. 로컬 주/보조 채널에서만 메시지를 재브로드캐스트. + 없음 + SENSOR, TRACKER 및 TAK_TRACKER role에서만 허용되며 CLIENT_MUTE role과 마찬가지로 모든 재브로드캐스트를 금지합니다. + 스텔스 또는 절전을 위해 필요한 경우에만 전송. + 다른 장치에서 온 패킷을 전달하지 않는 장치. + 분실 장치의 회수를 돕기 위해 기본 채널에 정기적으로 위치 정보를 전송. + 텔레메트리 패킷을 우선적으로전송. + ATAK 시스템 통신에 최적화됨, 정기적 전송을 최소화. + TAK PLI 전송을 자동화하고 정기적 전송을 최소화. + GPS 위치 정보를 우선적으로 전송. + 나침반 방향 + 디스플레이 모드 + 화면 뒤집기 + 상태표시줄 볼드체 + OLED 타입 + 단위 표시 + 12시간제 보기 + 활성화 하면 장치의 디스플레이에서 시간이 12시간제로 표시됩니다. + 이 설정은 기기에 가속도계가 내장되어 있어야 사용할 수 있습니다. + LED 출력 active high + 알림 벨 LED + 알림 벨 부저 + 알림 벨 진동 + 알림 메시지 LED + 알림 메시지 소리 + 알림 메시지 진동 + 외부 알림 활성화 + LED 출력 (GPIO) + 부저 출력 (GPIO) + 진동 출력 (GPIO) + I2S 부저 사용 + PWM 부저 사용 + 대역폭 + 주파수 슬롯 + Coding rate + Hop 제한 + MQTT로 부터 수신 무시 + Duty Cycle 무시 + PA fan 비활성화됨 + 지역 + 전송 활성화 + 전송 출력 + 프리셋 사용 + 메시지 + 서버 주소 + MQTT 활성화 + 암호화 사용 + 비밀번호 + Proxy to client 사용 + Root topic + 사용자명 + 이웃 정보 활성화 + LoRa로 전송 + MQTT 및 PhoneAPI로 전송하는 것 외에도, 우리 NeighborInfo는 LoRa를 통해 전송되어야 합니다. 기본 키와 이름을 사용하는 채널에서는 사용할 수 없습니다. + IPv4 모드 + 이더넷 활성화 + DNS + 게이트웨이 + IP + 서브넷 + NTP 서버 + 없음 + rsyslog 서버 + 비밀번호 + SSID + BLE RSSI 임계값 (기본값 -80) + 팍스카운터 활성화 + 활성화 + 전송 간격 + 타임스탬프 + 저전력 모드 설정 + 거리 테스트 활성화 + .CSV 파일 저장 (EPS32만 동작) + Admin 키 + 관리 모드 + 개인 키 + 공개 키 + 시리얼 콘솔 + 시리얼 baud rate + 시리얼 baud rate + 에코 활성화 + 시리얼 활성화 + 시리얼 모드 + 기본값 + 기본값 + 시간 초과 + 서버 + 역할 + 파랑 + 초록 + 빨강 + 대기질 메트릭 모듈 사용 + 환경 메트릭에서 화씨 사용 + 환경 메트릭 모듈 사용 + 환경 메트릭 화면 사용 + 전력 메트릭 모듈 사용 + 전력 메트릭 화면 사용 + diff --git a/core/resources/src/commonMain/composeResources/values-ko/strings.xml b/core/resources/src/commonMain/composeResources/values-ko/strings.xml index 2a198249e1..724bf946e0 100644 --- a/core/resources/src/commonMain/composeResources/values-ko/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ko/strings.xml @@ -17,6 +17,8 @@ --> + 습도 + 온도 앱에 대하여 수락 @@ -25,21 +27,12 @@ 추가 추가 서버 주소 - Admin 키 관리 고급 고급 - 대기질 메트릭 모듈 사용 지난 1시간 동안 전송에 사용된 통신 시간의 백분율. - - 알림 벨 부저 - 알림 벨 LED 알람 종 문자! - 알림 벨 진동 - 알림 메시지 소리 - 알림 메시지 LED - 알림 메시지 진동 All 입력 소스 허용 고도 @@ -55,20 +48,13 @@ 오디오 오디오 설정 나쁨 - - 대역폭 배터리 - BLE RSSI 임계값 (기본값 -80) - 파랑 블루투스 블루투스 설정 - 블루투스 활성화 설정 블루투스 - 상태표시줄 볼드체 설정 - 전송 간격 계산 중... 카메라 권한 부여 취소 @@ -88,7 +74,6 @@ 채널 7 채널 8 이 채널 URL은 유효하지 않으며 사용할 수 없습니다. - 채널 채널명 채널 @@ -98,18 +83,11 @@ 삭제 클라이언트 알림 닫기 - CODEC2 활성화 - CODEC2 샘플 레이트 - Coding rate 나침반 상단을 북쪽으로 고정 - 나침반 방향 손상된 키가 감지되었습니다. 다시 생성하려면 OK를 선택하세요. - 가속도계가 있는 장치를 두 번 탭하여 사용자 버튼과 동일한 동작. 장치에서 깜빡이는 LED를 제어합니다. 대부분 장치의 경우 최대 4개의 LED 중 하나를 제어할 수 있지만 충전 상태 LED와 GPS 상태 LED는 제어할 수 없습니다. - MQTT 및 PhoneAPI로 전송하는 것 외에도, 우리 NeighborInfo는 LoRa를 통해 전송되어야 합니다. 기본 키와 이름을 사용하는 채널에서는 사용할 수 없습니다. - 이 설정은 기기에 가속도계가 내장되어 있어야 사용할 수 있습니다. 설정 연결 연결됨 @@ -144,8 +122,6 @@ 세부 정보 감지 센서 감지 센서 설정 - 감지 센서 활성화 - 디텍션 트리거 타입 장치 절전모드 @@ -157,43 +133,29 @@ 날짜 직접 연결 메시지기기 - 화면 - 디스플레이 모드 - 활성화 하면 장치의 디스플레이에서 시간이 12시간제로 표시됩니다. - 단위 표시 거리 - DNS MQTT 노드 다운로드 - 에코 활성화 + 편집 8 시간 - 저전력 모드 설정 활성화 - - 암호화 사용 공개 키가 일치하지 않습니다 공개 키 암호화 - 환경 - 환경 메트릭 모듈 사용 - 환경 메트릭 화면 사용 - 환경 메트릭에서 화씨 사용 오류 듀티 사이클 제한에 도달했습니다. 지금은 메시지를 보낼 수 없습니다. 나중에 다시 시도하세요. - 이더넷 활성화 위치 교환 만료 설정 내보내기 외부 알림 외부 알림 설정 - 외부 알림 활성화 공장초기화 보통 Meshtastic %1$s @@ -207,26 +169,12 @@ 이 장치의 펌웨어가 매우 오래되어 이 앱과 호환되지않습니다. 더 자세한 정보는 펌웨어 업데이트 가이드를 참고해주세요. 펌웨어 업데이트가 필요합니다. 업데이트 실패 - 고정 PIN - 화면 뒤집기 주파수 - 주파수 슬롯 - 식별 이름 가스 저항 - 게이트웨이 - 반시계방향 동작 - 시계방향 동작 - 누름 동작 좋음 - GPIO 핀 - 로터리 엔코더 A포트 용 GPIO 핀 - 로터리 엔코더 B포트 용 GPIO 핀 - 로터리 엔코더 누름 포트 용 GPIO 핀 - 상태 모니터링 GPIO 핀 - 초록 하드웨어 하드웨어 모델 제목 @@ -235,17 +183,11 @@ 동의합니다. 위 내용을 읽고 이해했습니다. 저는 MQTT를 통해 제 노드 데이터를 암호화되지 않은 상태로 전송하는 것에 자발적으로 동의합니다. 뭘하는지 알고 있습니다 - I2S 시간 - I2S 데이터 in - I2S 데이터 out - I2S 단어 선택 IAQ (실내공기질) Bosch BME680으로 측정한 상대적 척도 IAQ 값. 범위 0–500. - 무시하기 %1$s를 무시 목록에 추가하시겠습니까? 수신 무시 - MQTT로 부터 수신 무시 %1$s를 무시 목록에서 삭제하시겠습니까? 설정 불러오기 @@ -259,13 +201,11 @@ IP IP 주소: 포트: - IPv4 모드 JSON 사용 최근 위치 업데이트 위도 - LED 상태 이 옵션을 활성화하면 암호화가 비활성화되며 기본 Meshtastic 네트워크와 호환되지 않습니다. @@ -285,10 +225,8 @@ %1$s 노드의 배터리가 낮습니다. (%2$d%) 배터리 부족: %1$s 조도 - 관리 모드 수동 위치 요청 필요함 - 캐시 용량: %1$dMB\n캐시 사용량: %2$dMB 캐시 관리자 현재 캐시 크기 %1$d 타일 @@ -303,11 +241,8 @@ 오프라인 관리자 SQL 캐시 제거 실패, 자세한 내용은 logcat 참조 %1$s에 대한 SQL 캐시가 제거되었습니다. - 맵 보고 MQTT를 통해 암호화되지 않은 노드 데이터를 공유하는 데 동의합니다. 이 기능을 활성화함으로써, 귀하는 귀하의 장치의 실시간 지리적 위치가 MQTT 프로토콜을 통해 암호화 없이 전송되는 것을 인지하고 동의합니다. 이 위치 데이터는 실시간 지도 보고, 장치 추적, 관련 텔레메트리 기능 등과 같은 목적으로 사용될 수 있습니다. - 맵 보고 간격 (초) - 귀하의 노드는 설정된 MQTT 서버로 주기적으로 암호화되지 않은 지도 보고서 패킷을 전송합니다. 이 패킷에는 ID, 긴 이름과 짧은 이름, 대략적인 위치, 하드웨어 모델, 역할, 펌웨어 버전, LoRa 지역, 모뎀 프리셋 및 주요 채널 이름이 포함됩니다. 다운로드 지역 선택 다운로드 시작 방위: %1$d° 거리: %2$s @@ -337,13 +272,11 @@ 전송 대기 열에 추가됨 알 수 없는 메시지기기 - 최소 전송 간격 (초) 모듈 설정 모듈 잠금해제 MQTT MQTT 설정 - MQTT 활성화 연결됨 연결 끊김 지역을 설정해 주세요! @@ -353,12 +286,10 @@ 8 시간 항상 알림 끄기 - 반복 종료 시간 (초) 이름 뒤로 가기 이웃 정보 이웃 정보 설정 - 이웃 정보 활성화 네트워크 새로운 채널 URL 수신 @@ -382,18 +313,17 @@ 즐겨찾기 우선 MQTT 경유 노드 목록 리셋 - 노드 정보 발송 주기 노드 지우기 없음 (연결해제) 없음 연결되지 않음 + 경고/벨 수신 알림 메시지 수신 알림 지금 - NTP 서버 확인 @@ -401,33 +331,21 @@ 1주 즐겨찾기만 보기 - - 부저 출력 (GPIO) - 출력 지속시간 (밀리초) - LED 출력 active high - LED 출력 (GPIO) - 진동 출력 (GPIO) - Duty Cycle 무시 - PA fan 비활성화됨 - 페어링 모드 비밀번호 팍스카운터 팍스카운터 설정 - 팍스카운터 활성화 핀 + 위치 위치 활성화 위치 - 전원 전원 설정 - 전력 메트릭 모듈 사용 - 전력 메트릭 화면 사용 언어 시스템 기본값 누르고 드래그해서 순서 변경 @@ -435,11 +353,8 @@ 주 채널 위치 및 텔레메트리 주기적 전송 - 개인 키 메쉬에 현재 위치 공유 - Proxy to client 사용 PSK - PTT 핀 공개 키 공개 키 변경됨 QR코드 @@ -453,21 +368,10 @@ 무선 설정 거리 테스트 거리 테스트 설정 - 거리 테스트 활성화 반응 재부팅 - - 중계 모드 - 관찰된 메시지가 우리 비공개 채널에 있거나, 동일한 LoRa 파라미터를 사용하는 다른 메쉬에서 온 경우 해당 메시지를 재전송합니다. - ALL 역할과 동일하게 동작하지만, 패킷 디코딩을 건너뛰고 단순히 재전송만 수행합니다. Repeater 일때 설정가능. 다른 Role에서는 ALL로 동작. - TAK, RangeTest, PaxCounter 등과 같은 비표준 포트 번호의 패킷을 무시합니다. NodeInfo, Text, Position, Telemetry 및 Routing과 같은 표준 포트 번호가 있는 패킷만 재브로드캐스트. - LOCAL_ONLY와 유사하게 외부 메쉬에서 관찰된 메시지를 무시하지만, 추가적으로 알려진 목록에 없는 노드의 메시지도 무시합니다. - 오픈되어 있거나 해독할 수 없는 외부 메시에서 관찰된 메시지를 무시합니다. 로컬 주/보조 채널에서만 메시지를 재브로드캐스트. - SENSOR, TRACKER 및 TAK_TRACKER role에서만 허용되며 CLIENT_MUTE role과 마찬가지로 모든 재브로드캐스트를 금지합니다. - 빨강 개인 키를 다시 생성하시겠습니까?\n\n이 노드와 이전에 키를 교환한 노드들은 해당 노드를 제거하고 키를 다시 교환해야 안전한 통신을 재개할 수 있습니다. 개인 키 다시 생성하기 - 지역 원격 원격 설정 @@ -486,19 +390,6 @@ 기본값으로 재설정 벨소리 - 앱과 연결해서 사용하거나 독립형 메시징 장치. - 스텔스 또는 절전을 위해 필요한 경우에만 전송. - 다른 장치에서 온 패킷을 전달하지 않는 장치. - 최소한의 오버헤드로 메시지를 전달하여 네트워크 범위를 확장하기 위한 인프라 노드. 노드 목록에 표시되지 않음. - CLIENT와 ROUTER의 조합. 이동형 장치에는 적합하지 않음. - 메시지를 중계하여 네트워크 범위를 확장하는 인프라 노드. 노드 목록에 표시. - 모든 다른 모드의 노드들이 패킷을 재전송한 후에만 항상 한 번씩 패킷을 재전송하여, 로컬 클러스터에 추가적인 커버리지를 보장하는 인프라스트럭처 노드입니다. 노드 목록에 표시. - 텔레메트리 패킷을 우선적으로전송. - ATAK 시스템 통신에 최적화됨, 정기적 전송을 최소화. - TAK PLI 전송을 자동화하고 정기적 전송을 최소화. - GPS 위치 정보를 우선적으로 전송. - Root topic - 로터리 엔코더 #1 활성화 수락 거부됨 루트 없음 @@ -506,12 +397,9 @@ 시간 초과됨 RSSI 수신 신호 강도 지표 Received Signal Strength Indicator, RSSI는 안테나가 수신하는 신호의 전력 수준을 측정하는 데 사용되는 지표입니다. RSSI 값이 높을수록 일반적으로 더 강력하고 안정적인 연결을 나타냅니다. - rsyslog 서버 인공위성 - 저장 저장 - .CSV 파일 저장 (EPS32만 동작) 스캔 보조 채널 @@ -521,17 +409,8 @@ 취소 전부 선택 보내기 - 벨 전송 - 알람 메시지와 벨 전송 - 송신 장치 메시지 간격 (초) - 시리얼 - 시리얼 baud rate 시리얼 설정 - 시리얼 콘솔 - 시리얼 활성화 - 시리얼 모드 - 서버 지역을 설정하세요 설정 @@ -553,18 +432,12 @@ 슬롯 SNR 통신에서 원하는 신호의 수준을 배경 잡음의 수준과 비교하여 정량화하는 데 사용되는 신호 대 잡음비 Signal-to-Noise Ratio, SNR는 Meshtastic와 같은 무선 시스템에서 SNR이 높을수록 더 선명한 신호를 나타내어 데이터 전송의 안정성과 품질을 향상시킬 수 있습니다. - SSID - 상태 전송 간격 (초) - 서브넷 지원됨 삭제 음소거 음소거 해제 서버 - 파랑 - 초록 - 빨강 텔레메트리 텔레메트리 설정 테마 @@ -574,7 +447,6 @@ 시간 시간 초과 타임스탬프 - TLS 사용 내 위치 토글 추적 루트 @@ -588,7 +460,6 @@ 모듈 활성화 - LoRa로 전송 BLE LoRa @@ -596,9 +467,9 @@ 24시간 48 시간 2주 - 전송 활성화 - 전송 출력 타입 + dBm + m 시스템 기본값 @@ -609,22 +480,13 @@ 감시되지 않거나 인프라 노드 음소거 해제 확인되지 않음 - 업/다운/선택 입력 활성화 - 업데이트 간격 (초) 업타임 URL - - 12시간제 보기 - I2S 부저 사용 - INPUT_PULLUP 모드 사용 - 프리셋 사용 - PWM 부저 사용 사용자 사용자 설정 유저 ID - 사용자명 MQTT 경유 전압 웨이포인트 삭제? diff --git a/core/resources/src/commonMain/composeResources/values-lt/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-lt/schema_strings.xml new file mode 100644 index 0000000000..58915174f1 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-lt/schema_strings.xml @@ -0,0 +1,43 @@ + + + + + Raudona + Atšaukti + Nėra + Pavadinimas + Taip pat kaip ir VISI bet nebando dekoduoti paketų ir juos tiesiog persiunčia. Galima naudoti tik Repeater rolės įtaise. Įjungus bet kokiame kitame įtaise - veiks tiesiog kaip VISI. + Tik žinomi + Nėra + Leidžiama tik SENSOR, TRACKER ar TAK_TRACKER rolių įtaisams. Tai užblokuos visas retransliacijas, ne taip kaip CLIENT_MUTE atveju. + Įtaisas transliuojantis tik prireikus. Naudojama slaptumo ar energijos taupymui. + Įtaisas kuris nepersiunčia kitų įtaisų paketų. + Reguliariai siunčia GPS pozicijos informaciją į pagrindinį kanalą, lengvesniam įtaiso radimui. + Pirmenybinis telemetrijos paketų siuntimas + Optimizuota ATAK komunikacijai, sumažinta rutininių transliacijų + Įgalina automatines TAK PLI transliacijas ir sumažina rutininių transliacijų kiekį. + Pirmenybinis GPS pozicijos paketų siuntimas + Regionas + Žinutė + Nėra + Išsaugoti + Privatus raktas + Viešasis raktas + Baigėsi laikas + Raudona + diff --git a/core/resources/src/commonMain/composeResources/values-lt/strings.xml b/core/resources/src/commonMain/composeResources/values-lt/strings.xml index 3ce90ee955..4fda55d892 100644 --- a/core/resources/src/commonMain/composeResources/values-lt/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-lt/strings.xml @@ -17,6 +17,8 @@ --> + Drėgmė + Temperatūra Apie Priimti @@ -27,7 +29,6 @@ Administravimas Procentas eterio laiko naudoto perdavimams per pastarąją valandą. - Skambučio simbolis! Reikalingas programos atnaujinimas @@ -36,7 +37,6 @@ Geras Ar tikrai norite pakeisti į numatytąjį kanalą? Silpnas - Baterija Skaičiuojama… @@ -47,7 +47,6 @@ Dabartinio kanalo panaudojimas, įskaitant gerai suformuotą TX (siuntimas), RX (gavimas) ir netinkamai suformuotą RX (arba - triukšmas). Šio kanalo URL yra neteisingas ir negali būti naudojamas - Kanalas Kanalo pavadinimas Pasirinkite Aplinką @@ -81,17 +80,15 @@ Atsijungta Tiesiogiai - Atstumas + Redaguoti 8 Valandos - Viešojo rakto neatitikimas Viešojo rakto šifruotė - Klaida Pasiektas veikimo ciklo limitas. Šiuo metu negalima siųsti žinučių, bandykite vėliau. @@ -106,9 +103,7 @@ Geras - Persiuntimų kiekis - Ignoruoti Ar pridėti „%1$s“ į ignoruojamų sąrašą? Po šio pakeitimo jūsų radijas bus perkrautas. Ar pašalinti „%1$s“ iš ignoruojamų sąrašo? Po šio pakeitimo jūsų radijas bus perkrautas. @@ -129,7 +124,6 @@ Log`ai - Talpyklos talpa: %1$d MB\nTalpyklos naudojimas: %2$d MB Talpyklos valdymas Dabartinis talpyklos dydis %1$d plytelės @@ -189,6 +183,7 @@ Nėra (išjungti) Nėra Neprijungtas + Gerai @@ -197,17 +192,15 @@ 1 sav Atidaryti nustatymus - + - Kalba Numatytoji sistema - Privatus raktas Pateikti telefono vietą tinklui Viešasis raktas QR kodas @@ -219,12 +212,6 @@ Naujas greitas pokalbis Radijo modulio konfigūracija Perkrauti - - Persiųsti visas žinutes, nesvarbu jos iš Jūsų privataus tinklo ar iš kito tinklo su analogiškais LoRa parametrais. - Taip pat kaip ir VISI bet nebando dekoduoti paketų ir juos tiesiog persiunčia. Galima naudoti tik Repeater rolės įtaise. Įjungus bet kokiame kitame įtaise - veiks tiesiog kaip VISI. - Leidžiama tik SENSOR, TRACKER ar TAK_TRACKER rolių įtaisams. Tai užblokuos visas retransliacijas, ne taip kaip CLIENT_MUTE atveju. - Raudona - Regionas Nuotolinis administravimas @@ -238,23 +225,12 @@ Nustatyti iš naujo Atkurti numatytuosius parametrus - Programėlė prijungta prie atskiro susirašinėjimo įtaiso. - Įtaisas transliuojantis tik prireikus. Naudojama slaptumo ar energijos taupymui. - Įtaisas kuris nepersiunčia kitų įtaisų paketų. - Stacionarus įtaisas tinklo išplėtimui, persiunčiantis žinutes. Nerodomas įtaisų sąraše. - ROUTER ir CLIENT kombinacija. Neskirta mobiliems įtaisams. - Stacionarus aukštuminis įtaisas geresniam tinklo padengimui. Matomas node`ų sąraše. - Pirmenybinis telemetrijos paketų siuntimas - Optimizuota ATAK komunikacijai, sumažinta rutininių transliacijų - Įgalina automatines TAK PLI transliacijas ir sumažina rutininių transliacijų kiekį. - Pirmenybinis GPS pozicijos paketų siuntimas Gautas negatyvus patvirtinimas Nėra maršruto Pristatymas patvirtintas Baigėsi laikas RSSI - Išsaugoti Išsaugoti @@ -262,7 +238,6 @@ Pažymėti visus Siųsti - Dalintis Dalintis su… @@ -275,7 +250,6 @@ Ištrinti Nutildyti - Raudona Išvaizda Tamsi Šviesi @@ -304,7 +278,6 @@ Nežinomas vartotojo vardas Be kategorijos - per MQTT Ištrinti orientyrą? diff --git a/core/resources/src/commonMain/composeResources/values-nl/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-nl/schema_strings.xml new file mode 100644 index 0000000000..371cd76e9c --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-nl/schema_strings.xml @@ -0,0 +1,105 @@ + + + + + Blauw + Huidige + Groen + LED status + Rood + Standaard + CODEC2 sample rate + CODEC 2 ingeschakeld + I2S klok + PTT pin + Bluetooth ingeschakeld + Koppelmodus + Genereer invoergebeurtenis bij indrukken + Annuleer + Geen + Verstuur bel + Detectie trigger type + Bewegingssensor Ingeschakeld + GPIO pin om te monitoren + Weergavenaam + Verstuur bel + Behandel een dubbele tik op ondersteunde versnellingsmeters als een knopindruk door de gebruiker. + Alles + Hetzelfde gedrag als ALL maar sla pakketdecodering over en herzendt opnieuw. Alleen beschikbaar in Repeater rol. Het instellen van dit op andere rollen resulteert in ALL gedrag. + Negeert waargenomen berichten van open vreemde mazen of die welke niet kunnen decoderen. Alleen heruitzenden bericht op de nodes lokale primaire / secundaire kanalen. + Geen + Alleen toegestaan voor SENSOR, TRACKER en TAK_TRACKER rollen, dit zal alle heruitzendingen beperken, niet in tegenstelling tot CLIENT_MUTE rol. + Apparaat dat alleen uitzendt als dat nodig is voor stealth of energiebesparing. + Apparaat stuurt geen pakketten van andere apparaten. + Zend locatie regelmatig als bericht via het standaard kanaal voor zoektocht apparaat. + Zendt telemetriepakketten met prioriteit uit. + Geoptimaliseerd voor ATAK-systeemcommunicatie, beperkt routine uitzendingen. + Activeer automatisch zenden TAK PLI en beperk routine zendingen. + Zendt GPS-positiepakketten met prioriteit uit. + Kompas oriëntatie + Weergavemodus + Scherm omdraaien + Geef eenheden weer + Output vibra (GPIO) + Gebruik PWM zoemer + Bandbreedte + Negeer MQTT + Overschrijf Duty Cycle + Regio + Bericht + Adres + MQTT ingeschakeld + Encryptie ingeschakeld + Wachtwoord + Proxy to client ingeschakeld + Gebruikersnaam + Zend over LoRa + IPv4 modus + Ethernet ingeschakeld + Gateway + IP-adres + Subnet + NTP server + Geen + rsyslog server + Wachtwoord + SSID + BLE RSSI drempelwaarde (standaard -80) + Paxcounter ingeschakeld + Tijdstempel + Energiebesparingsmodus inschakelen + Opslaan + Admin Sleutel + Beheerde modus + Privésleutel + Publieke sleutel + Seriële console + Echo ingeschakeld + Serieel ingeschakeld + Seriële modus + Standaard + Standaard + Time-Out + Hartslag + Server + Aantal records + Functie + Blauw + Groen + Rood + diff --git a/core/resources/src/commonMain/composeResources/values-nl/strings.xml b/core/resources/src/commonMain/composeResources/values-nl/strings.xml index bec60d9a06..f59bd98675 100644 --- a/core/resources/src/commonMain/composeResources/values-nl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-nl/strings.xml @@ -17,6 +17,8 @@ --> + Vochtigheid + Temperatuur Over Accepteer @@ -26,11 +28,9 @@ Voeg toe Voeg toe Adres - Admin Sleutel Beheer Percentage van de zendtijd die het afgelopen uur werd gebruikt. - Melding Bell teken! Alles Alt @@ -46,15 +46,10 @@ Audioconfiguratie Beschikbare pinnen Slecht - - Bandbreedte Batterij - BLE RSSI drempelwaarde (standaard -80) - Blauw Bluetooth Bluetooth Configuratie - Bluetooth ingeschakeld Bluetooth Instellingen Berekenen… @@ -73,7 +68,6 @@ Kanaal 7 Kanaal 8 Deze Kanaal URL is ongeldig en kan niet worden gebruikt - Kanaal Kanaalnaam Kanalen @@ -82,13 +76,9 @@ Wis Sluit - CODEC 2 ingeschakeld - CODEC2 sample rate Kompas Noorden bovenaan - Kompas oriëntatie - Behandel een dubbele tik op ondersteunde versnellingsmeters als een knopindruk door de gebruiker. Regelt de knipperende LED op het apparaat. Voor de meeste apparaten betreft dit een van de maximaal 4 LEDs; de LED's van de lader en GPS zijn niet regelbaar. Verbinding maken Verbonden @@ -116,8 +106,6 @@ Details Detectie Sensor Bewegingssensor Configuratie - Bewegingssensor Ingeschakeld - Detectie trigger type Apparaat Apparaat in slaapstand @@ -127,29 +115,21 @@ Direct Berichten - Weergave - Weergavemodus - Geef eenheden weer Afstand MQTT + Dynamisch - Echo ingeschakeld Wijzig 8 Uur - Energiebesparingsmodus inschakelen - - Encryptie ingeschakeld Publieke sleutel komt niet overeen Publieke sleutel encryptie - Omgeving Fout Limiet van Duty Cycle bereikt. Kan nu geen berichten verzenden, probeer het later opnieuw. - Ethernet ingeschakeld Positie uitwisselen Configuratie exporteren @@ -166,33 +146,21 @@ De radio firmware is te oud om met deze applicatie te praten. Voor meer informatie over deze zaak, zie onze Firmware Installation gids. Firmware-update vereist. Bijwerken mislukt - Vaste PIN - Scherm omdraaien Freq - Weergavenaam - Gateway - Genereer invoergebeurtenis bij indrukken Goed - GPIO pin - GPIO pin om te monitoren - Groen Hardware Hardwaremodel - Hartslag Wachtwoord verbergen Aantal sprongen Ik weet waar ik mee bezig ben. - I2S klok IAQ (Binnenluchtkwaliteit) relatieve schaal IAQ waarde gemeten door Bosch BME680. Waarde tussen 0 en 500. - Negeer Voeg '%1$s' toe aan negeerlijst? Inkomende negeren - Negeer MQTT Verwijder '%1$s' uit negeerlijst? Configuratie importeren @@ -205,12 +173,10 @@ IP-adres IP-adres: Poort: - IPv4 modus JSON uitvoer ingeschakeld Breedtegraad - LED status Legacy Admin kanaal @@ -226,10 +192,8 @@ LoRa Batterij bijna leeg: %1$s Lux - Beheerde modus Handmatige positieaanvraag vereist - Cache capaciteit: %1$d MB\nCache gebruik: %2$d MB Cachemanager Huidige cache grootte %1$d tegels @@ -242,7 +206,6 @@ Offline Manager SQL cache verwijderen mislukt, zie logcat voor details SQL cache gewist voor %1$s - Kaartrapportage Selecteer regio om te downloaden Download starten richting: %1$d° afstand: %2$s @@ -265,12 +228,10 @@ Bericht In behandeling Berichten - Minimale broadcast (seconden) Module-configuratie MQTT MQTT Configuratie - MQTT ingeschakeld Verbonden Niet verbonden Je moet een regio instellen! @@ -307,33 +268,26 @@ Geen (uit) Geen Niet verbonden + - NTP server - Aantal records OK 24 Uur 1W - - Output duur (milliseconden) - Output vibra (GPIO) - Overschrijf Duty Cycle - Koppelmodus Wachtwoord Paxcounter Paxcounter Configuratie - Paxcounter ingeschakeld + Positie Positie ingeschakeld Positie - Vermogen Energie configuratie Taal @@ -341,11 +295,8 @@ Druk Primair - Privésleutel Geef telefoon locatie door aan mesh - Proxy to client ingeschakeld PSK - PTT pin Publieke sleutel Publieke sleutel gewijzigd QR-code @@ -359,15 +310,6 @@ Radioconfiguratie Bereik Test Herstart - - Herzend ontvangen berichten indien ontvangen op eigen privé kanaal of van een ander toestel met dezelfde lora instellingen. - Hetzelfde gedrag als ALL maar sla pakketdecodering over en herzendt opnieuw. Alleen beschikbaar in Repeater rol. Het instellen van dit op andere rollen resulteert in ALL gedrag. - Negeert pakketten van niet-standaard portnums, zoals: TAK, RangeTest, PaxCounter, etc. Herzendt alleen pakketten met standaard portnummers: NodeInfo, Text, Positie, Telemetry, en Routing. - Negeert alleen waargenomen berichten van vreemde meshes zoals LOCAL ONLY, maar gaat een stap verder door ook berichten van knooppunten te negeren die nog niet in de bekende lijst van knooppunten staan. - Negeert waargenomen berichten van open vreemde mazen of die welke niet kunnen decoderen. Alleen heruitzenden bericht op de nodes lokale primaire / secundaire kanalen. - Alleen toegestaan voor SENSOR, TRACKER en TAK_TRACKER rollen, dit zal alle heruitzendingen beperken, niet in tegenstelling tot CLIENT_MUTE rol. - Rood - Regio Extern beheer Externe Hardware @@ -384,17 +326,6 @@ Standaardinstellingen terugzetten Beltoon - Toestel geeft geen pakketten door van andere toestellen. - Apparaat dat alleen uitzendt als dat nodig is voor stealth of energiebesparing. - Apparaat stuurt geen pakketten van andere apparaten. - Infrastructuur node om netwerk bereik te vergroten door berichten door te geven zonder overtolligheid. Niet zichtbaar in noden lijst. - Combinatie van ROUTER en CLIENT. Niet voor mobiele toestellen. - Infrastructuur node om netwerk bereik te vergroten door berichten door te geven. Zichtbare in noden lijst. - Infrastructuurknooppunt dat altijd pakketten één keer opnieuw uitzendt, maar pas nadat alle andere modi zijn voltooid, om extra dekking te bieden voor lokale clusters. Zichtbaar in de lijst met knooppunten. - Zendt telemetriepakketten met prioriteit uit. - Geoptimaliseerd voor ATAK-systeemcommunicatie, beperkt routine uitzendingen. - Activeer automatisch zenden TAK PLI en beperk routine zendingen. - Zendt GPS-positiepakketten met prioriteit uit. Negatieve bevestiging ontvangen Geen route @@ -402,9 +333,7 @@ Time-Out RSSI Ontvangen Signal Sterkte Indicator, een meting gebruikt om het stroomniveau te bepalen dat de antenne ontvangt. Een hogere RSSI-waarde geeft een sterkere en stabielere verbinding aan. - rsyslog server Sats - Opslaan Opslaan @@ -414,14 +343,8 @@ Beveiliging Selecteer alle Verzend - Verstuur bel - Serieel Seriële Configuratie - Seriële console - Serieel ingeschakeld - Seriële modus - Server instellingen Deel @@ -437,17 +360,12 @@ Slot SNR Signal-to-Noise Ratio, een meeting die wordt gebruikt in de communicatie om het niveau van een gewenst signaal tegenover achtergrondlawaai te kwantificeren. In Meshtastische en andere draadloze systemen geeft een hoger SNR een zuiverder signaal aan dat de betrouwbaarheid en kwaliteit van de gegevensoverdracht kan verbeteren. - SSID - Subnet Ondersteund Verwijder Demp Dempen opheffen Server - Blauw - Groen - Rood Telemetrie Thema Donker @@ -455,7 +373,6 @@ Systeemstandaard Time-Out Tijdstempel - TLS ingeschakeld Wissel mijn positie Traceroute @@ -468,7 +385,6 @@ Traceroute - Zend over LoRa LoRa MQTT @@ -484,17 +400,13 @@ Niet berichtbaar Dempen opheffen Niet herkend - Update-interval (seconden) Tijd online URL - - Gebruik PWM zoemer Gebruiker Gebruikersconfiguratie Gebruiker ID - Gebruikersnaam via MQTT Spanning Waypoint verwijderen? diff --git a/core/resources/src/commonMain/composeResources/values-no/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-no/schema_strings.xml new file mode 100644 index 0000000000..1a1d4eb82f --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-no/schema_strings.xml @@ -0,0 +1,43 @@ + + + + + Avbryt + Ingen + Navn + Behandle dobbeltrykk på støttede akselerometre som brukerknappetrykk. + Samme atferd som alle andre, men hopper over pakkedekoding og sender dem ganske enkelt på nytt. Kun tilgjengelig i Repeater-rollen. Å sette dette på andre roller vil resultere i ALL oppførsel. + Ignorerer observerte meldinger fra fremmede mesh'er som er åpne eller de som ikke kan dekrypteres. Sender kun meldingen på nytt på nodene lokale primære / sekundære kanaler. + Ingen + Bare tillatt for SENSOR, TRACKER og TAK_TRACKER roller, så vil dette hindre alle rekringkastinger, ikke i motsetning til CLIENT_MUTE rollen. + Enhet som bare kringkaster når nødvendig, for stealth eller strømsparing. + Enhet som ikke videresender pakker fra andre enheter. + Sender sted som melding til standardkanalen regelmessig for å hjelpe med å finne enheten. + Sender telemetripakker som prioritet. + Optimalisert for ATAK systemkommunikasjon, reduserer rutinemessige kringkastinger. + Aktiverer automatiske TAK PLI-sendinger og reduserer rutinesendinger. + Sender GPS-posisjonspakker som prioritert. + Region + Melding + Hvorvidt det i tillegg for å sende det til MQTT og til telefonen, skal vår Naboinfo overføres over LoRa. Ikke tilgjengelig på en kanal med standardnøkkel og standardnavn. + Ingen + Lagre + Privat nøkkel + Offentlig nøkkel + Tidsavbrudd + diff --git a/core/resources/src/commonMain/composeResources/values-no/strings.xml b/core/resources/src/commonMain/composeResources/values-no/strings.xml index 59d1f9561a..99009085d0 100644 --- a/core/resources/src/commonMain/composeResources/values-no/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-no/strings.xml @@ -17,6 +17,8 @@ --> + Luftfuktighet + Temperatur Om Godta @@ -27,7 +29,6 @@ Administrasjon Prosent av lufttiden brukt i løpet av den siste timen. - Varsel, bjellekarakter! Applikasjon for gammel @@ -36,7 +37,6 @@ Godt Er du sikker på at du vil endre til standardkanalen? Dårlig - Batteri Beregner… @@ -47,7 +47,6 @@ Utnyttelse for denne kanalen, inkludert godt formet TX, RX og feilformet RX (aka støy). Denne kanall URL er ugyldig og kan ikke benyttes - Kanal Kanal Navn Velg tema @@ -57,9 +56,7 @@ Lukk - Behandle dobbeltrykk på støttede akselerometre som brukerknappetrykk. Kontrollerer blinking av LED på enheten. For de fleste enheter vil dette kontrollere en av de opp til 4 lysdiodene. Laderen og GPS-lysene er ikke kontrollerbare. - Hvorvidt det i tillegg for å sende det til MQTT og til telefonen, skal vår Naboinfo overføres over LoRa. Ikke tilgjengelig på en kanal med standardnøkkel og standardnavn. Tilkoblet radio, men den sover Kopier @@ -82,17 +79,15 @@ Frakoblet Direkte - Distanse + Rediger 8 Timer - Direktemeldinger bruker den nye offentlige nøkkelinfrastrukturen for kryptering. Krever firmware versjon 2.5 eller høyere. Offentlig-nøkkel kryptering - Feil Grensen for sykluser er nådd. Kan ikke sende meldinger akkurat nå, prøv igjen senere. @@ -107,11 +102,9 @@ Godt - Hopp Unna Luftkvalitet (Innendørs luftkvalitet) relativ skala IAQ-verdi målt ved Bosch BME680. Verdi 0–500. - Ignorer Legg til '%1$s' i ignorereringslisten? Fjern '%1$s fra ignoreringslisten? @@ -132,7 +125,6 @@ Logger - Cache Kapasitet: %1$d MB\nCache Bruker: %2$d MB Mellomlagerbehandler Nåværende størrelse for mellomlager %1$d fliser @@ -192,6 +184,7 @@ Ingen (slå av) Ingen Ikke tilkoblet + Ok @@ -199,17 +192,15 @@ 24 Timer 1U - + - Språk System standard - Privat nøkkel Oppgi plassering til nett Offentlig nøkkel QR kode @@ -221,14 +212,6 @@ Ny enkelchat Radiokonfigurasjon Omstart - - Alle observerte meldinger sendes på nytt hvis den var på vår private kanal eller fra en annen mesh med samme lora-parametere. - Samme atferd som alle andre, men hopper over pakkedekoding og sender dem ganske enkelt på nytt. Kun tilgjengelig i Repeater-rollen. Å sette dette på andre roller vil resultere i ALL oppførsel. - Ignorerer pakker fra ikke-standard portnumre som: TAK, RangeTest, PaxCounter, etc. Kringkaster kun pakker med standard portnum: NodeInfo, Text, Position, Telemetrær og Ruting. - Ignorer observerte meldinger fra utenlandske mesher som KUN LOKALE men tar det steget videre, ved å også ignorere meldinger fra noder som ikke allerede er i nodens kjente liste. - Ignorerer observerte meldinger fra fremmede mesh'er som er åpne eller de som ikke kan dekrypteres. Sender kun meldingen på nytt på nodene lokale primære / sekundære kanaler. - Bare tillatt for SENSOR, TRACKER og TAK_TRACKER roller, så vil dette hindre alle rekringkastinger, ikke i motsetning til CLIENT_MUTE rollen. - Region Fjernadministrasjon @@ -241,17 +224,6 @@ Nullstill Tilbakestill til standard - App-tilkoblet eller frittstående meldingsenhet. - Enhet som bare kringkaster når nødvendig, for stealth eller strømsparing. - Enhet som ikke videresender pakker fra andre enheter. - Infrastruktur-node for utvidelse av nettverksdekning ved å videresende meldinger med minimal overhead. Ikke synlig i nodelisten. - Kombinasjon av ROUTER og CLIENT. Ikke for mobile enheter. - Infrastruktur-node for utvidelse av nettverksdekning ved å videresende meldinger. Synlig i nodelisten. - Infrastrukturnode som alltid sender pakker på nytt én gang, men bare etter alle andre moduser og sikrer ekstra dekning for lokale klynger. Synlig i nodelisten. - Sender telemetripakker som prioritet. - Optimalisert for ATAK systemkommunikasjon, reduserer rutinemessige kringkastinger. - Aktiverer automatiske TAK PLI-sendinger og reduserer rutinesendinger. - Sender GPS-posisjonspakker som prioritert. Mottok negativ bekreftelse Ingen rute @@ -259,7 +231,6 @@ Tidsavbrudd RSSI "Received Signal Strength Indicator", en måling som brukes til å bestemme strømnivået som mottas av antennen. Høyere RSSI verdi indikerer generelt en sterkere og mer stabil forbindelse. - Lagre Lagre @@ -267,7 +238,6 @@ Velg alle Send - Del Del med… @@ -307,7 +277,6 @@ Ukjent Brukernavn Ikke gjenkjent - via MQTT Fjern veipunkt? diff --git a/core/resources/src/commonMain/composeResources/values-pl/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-pl/schema_strings.xml new file mode 100644 index 0000000000..a820629572 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-pl/schema_strings.xml @@ -0,0 +1,225 @@ + + + + + Niebieski + Natężenie + Zielony + Stan diody LED + Czerwony + Domyślny + Częstotliwość próbkowania CODEC2 + CODEC 2 włączony + Zegar I2S + Pin PTT (Push-To-Talk) + Bluetooth włączony + Stały PIN + Tryb parowania + Stały PIN + Brak PINu (po prostu działa) + Wstecz + Anuluj + W Dół + W Lewo + Brak + W Prawo + Wybierz + W Górę + Wyślij Dzwonek + Czujnik detekcji włączony + Pin GPIO do monitorowania + Przyjazna nazwa + Wyślij Dzwonek + Użyj trybu INPUT_PULLUP + Przycisk GPIO + Buzzer GPIO + Wyłącz potrójne kliknięcie + Podwójne dotknięcie jako naciśnięcie przycisku + Traktuj podwójne dotknięcie na obsługiwanych akcelerometrach jako naciśnięcie przycisku użytkownika. + LED bicia serca + Interwał transmisji informacji o węźle + Tryb retransmisji + Wszystkie + Wszystkie, pomiń dekodowanie + To samo zachowanie co ALL, ale pomija dekodowanie pakietów i po prostu je retransmituje. Dostępne tylko w roli REPEATER. Ustawienie tego w innych rolach spowoduje zachowanie jak ALL. + Tylko znane + Tylko lokalne + Ignoruje odebrane pakiety z obcych sieci Mesh, które są otwarte lub których nie można odszyfrować. Retransmituje wiadomość tylko na lokalnych kanałach primary / secondary. + Brak + Dozwolone wyłącznie dla ról SENSOR, TRACKER i TAK_TRACKER. Spowoduje to zablokowanie wszystkich retransmisji, podobnie jak rola CLIENT_MUTE. + Rola urządzenia + Klient + Klient (domyślnie) - Klient połączony z aplikacją. + Used for nodes that "only speak when spoken to" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption. + Klient pasywny + Wyciszenie klienta - To samo, co klient, z wyjątkiem pakietów, które nie przeskakują przez ten węzeł, nie przyczynia się do routingu pakietów dla siatki. + Nadaje regularnie lokalizację jako wiadomości do głównego kanału, aby pomóc w odzyskaniu urządzenia. + Repeater + Router + Router Klienta + Router - Pakiety siatki będą preferować trasowanie przez ten węzeł. Zakłada, że urządzenie będzie działać samodzielnie, umieszczone w miejscu z przewagą zasięgu. UWAGA: Radia BLE/Wi-Fi i ekran OLED zostaną uśpione. + Czujnik + Nadaje priorytetowo pakiety telemetryczne. + TAK + Zoptymalizowany pod kątem komunikacji systemowej ATAK, redukuje nadmiarowe transmisje. + Umożliwia automatyczne transmisje TAK PLI i zmniejsza liczbę nadmiarowych transmisji. + Tracker - Do użytku z urządzeniami przeznaczonymi jako śledzenie GPS. Pakiety pozycyjne wysyłane z tego urządzenia będą miały wyższy priorytet, z nadawaniem pozycji co dwie minuty. Inteligentna transmisja pozycji będzie domyślnie wyłączona. + Strefa czasowa + Interwał karuzeli + Automatycznie przewija się na następną stronę na ekranie jak karuzela, co określony interwał czasowy. + Zawsze wskazywać na północ + Orientacja kompasu + Tryb wyświetlania + Nadpisz domyślny układ ekranu. + Odwróć ekran + Odwróć ekran pionowo. + Pogrubiony nagłówek + Typ ekranu OLED + Nadpisz automatyczne wykrywanie ekranu OLED. + Ekran włączony na + Jak długo ekran pozostaje włączony po naciśnięciu przycisku użytkownika lub odebraniu wiadomości. + Wyświetlana jednostka + Jednostki wyświetlane na ekranie urządzenia. + Użyj formatu 12-godzinnego + Wybudź przy dotknięciu lub ruchu + Wymagany jest akcelerometr na urządzeniu. + Aktywny + Powiadomienia zewnętrzne włączone + Nag Czas przerwy + Wyjście LED (GPIO) + Wyjście buzzera (GPIO) + Wyjście silnika wibracyjnego (GPIO) + Użyj I2S jako buzzer + Użyj buzzer PWM + Pasmo + Slot częstotliwości + Częstotliwość robocza węzła jest obliczana na podstawie regionu, ustawień modemu i tego pola. Gdy wartość wynosi 0, slot jest automatycznie obliczany na podstawie nazwy kanału podstawowego i zmienia się z domyślnego slotu publicznego. Jeśli skonfigurowano prywatny kanał podstawowy i publiczny kanał dodatkowy, należy przywrócić domyślny slot publiczny. + Szybkość kodowania + Ok dla MQTT + Limit przeskoków + Ustawia maksymalną liczbę przeskoków, domyślnie jest to 3. Zwiększenie liczby przeskoków powoduje również wzrost przeciążenia i należy stosować tę opcję ostrożnie. Komunikaty rozgłoszeniowe z 0 przeskokami nie otrzymają potwierdzeń ACK. + Zignoruj MQTT + Presety + Daleki zasięg — Szybko + Daleki zasięg — Średnio + Daleki zasięg — Wolno + Daleki zasięg — Turbo + Średni zasięg — Szybko + Średni zasięg — wolno + Krótki zasięg — Szybko + Bliski zasięg — Wolno + Krótki zasięg — Turbo + Bardzo daleki zasięg — Wolno + Nadpisz cykl pracy + Region + Region, w którym będziesz używać urządzenia. + Nadawanie włączone + Moc nadawania + Użyj predefiniowanych ustawień + Wiadomość + Adres + Włącz MQTT + Szyfrowanie włączone + Twój węzeł będzie okresowo wysyłał niezaszyfrowany pakiet raportów map na skonfigurowany serwer MQTT, co obejmuje identyfikację, krótką i długą nazwę, przybliżona lokalizacja, model sprzętowy, role, wersja oprogramowania, region LoRa, ustawienie modemu i nazwa kanału głównego. + Hasło + Klient Proxy MQTT + Główny temat + TLS włączony + Nazwa użytkownika + Włącz informacje o sąsiedzie + Nadaj przez LoRa + Czy oprócz wysyłania do MQTT i PhoneAPI, NeighborInfo powinny być przesłane przez LoRa? Niedostępny na kanale z domyślnym kluczem i nazwą. + Częstotliwość aktualizacji + Tryb IPv4 + Włącz nadawanie pakietów poprzez UDP w lokalnej sieci. + Ethernet włączony + Włączenie połączenia Ethernet spowoduje wyłączenie bluetootha. Połączania TCP nie są dostępne na urządzeniach Apple. + DNS + Brama domyślna + IP + Podsieć + Serwer NTP + Brak + Transmisja UDP + Serwer rsyslog + Włączenie WiFi spowoduje wyłączenie bluetootha. + Hasło + SSID + Częstotliwość aktualizacji + Minimalna Odległość + Minimalna zmiana odległości w metrach, którą należy uwzględnić w przypadku inteligentnego pozycjonowania. + Minimalny interwał + Położenie stałe + GPS EN GPIO + Tryb GPS + Częstotliwość aktualizacji + Jak często powinniśmy próbować uzyskać pozycję GPS (<10 sekund utrzymuje GPS włączony). + Wyłączony + Włączony + Interwał transmisji + Maksymalny odstęp czasu, jaki może upłynąć bez nadawania lokalizacji przez węzeł. + Inteligentne Pozycjonowanie + Flagi położenia + Opcjonalne pola dołączane do danych lokalizacji. Im więcej pól, tym większy rozmiar pakietu, co wydłuża czas transmisji i zwiększa ryzyko jego utraty. + Wysokość + Wysokość n.p.m. to średni poziom morza + Rozdzielenie geoidalne wysokości + Kurs pojazdu + Liczba satelitów + Numer sekwencji + Prędkość pojazdu + Znacznik czasu + GPS Rx GPIO + GPS Tx GPIO + Włącz tryb oszczędzania energii + Uśpij wszystko na tak długo, jak to możliwe, w przypadku funkcji trackera i czujnika obejmie to również radio lora. Nie używaj tego ustawienia, jeśli chcesz korzystać z urządzenia z aplikacjami na telefon lub używasz urządzenia bez przycisków. + Zapisz + Klucz administratora + Klucz publiczny uprawniony do wysyłania wiadomości administracyjnych do tego węzła. + Pokaż na żywo logi debugowania przez połączenie szeregowe, podejrzyj i eksportuj logi węzła (bez informacji lokalizacyjnych) przez Bluetooth. + Urządzenie jest zarządzane przez administratora sieci mesh, użytkownik nie ma dostępu do żadnych ustawień urządzenia. + Klucz prywatny + Używane do tworzenia klucza współdzielonego ze zdalnym urządzeniem + Klucz publiczny + Konsola szeregowa przez Stream API. + Prędkość transmisji + Prędkość transmisji + Włącz echo + Włącz tryb serial + Tryb serial + Domyślny + Domyślny + Pozycje NMEA + Protobufy + Prosty + Wiadomość tekstowa + Limit czasu + Bicie serca + Serwer + Liczba rekordów + Rola + Niebieski + Zielony + Czerwony + Włącz moduł metryk jakości powietrza + Czas aktualizacji metryk jakości powietrza + Wyświetl Farenheita + Włącz moduł metryk zasilania + Wyświetlaj metryki zasilania na ekranie + Czas aktualizacji metryk zasilania + diff --git a/core/resources/src/commonMain/composeResources/values-pl/strings.xml b/core/resources/src/commonMain/composeResources/values-pl/strings.xml index b4d21584f1..83eea1f836 100644 --- a/core/resources/src/commonMain/composeResources/values-pl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pl/strings.xml @@ -17,6 +17,8 @@ --> + Wilgotność + Temperatura O aplikacji Akceptuj Potwierdzenia @@ -29,7 +31,6 @@ Dodaj warstwę Dodaj Adres - Klucz administratora Klucze administratora Zarządzanie Zaawansowane @@ -37,14 +38,10 @@ Zaawansowane Ikona jakości powietrza - Włącz moduł metryk jakości powietrza - Czas aktualizacji metryk jakości powietrza Procent czasu wykorzystanego do transmisji w ciągu ostatniej godziny. - Znak ostrzegawczy! Wszystkie Wysokość - Zawsze wskazywać na północ Oświetlenie otoczenia Zezwalaj na analizę i raportowanie awarii. @@ -62,28 +59,19 @@ Wstecz Klucze zapasowe słaby - - Pasmo Bateria - Niebieski Bluetooth Konfiguracja Bluetooth - Bluetooth włączony Konfiguracja Bluetooth - Pogrubiony nagłówek Ustawienia - Interwał transmisji - Przycisk GPIO - Buzzer GPIO Obliczanie… Pozwolenie na użycie kamery Anuluj Konfiguracja gotowych wiadomości Nie można zmienić kanału, ponieważ urządzenie nie jest jeszcze podłączone. Proszę, spróbuj ponownie. Wyłączenie nie jest obsługiwane w tym urządzeniu - Interwał karuzeli Wykorzystanie dla bieżącego kanału, w tym prawidłowego TX/RX oraz zniekształconego RX (czyli szumu). Kanał 1 @@ -95,7 +83,6 @@ Kanał 7 Kanał 8 Ten adres URL kanału jest nieprawidłowy i nie można go użyć - Kanał Nazwa Kanału Kanały @@ -110,9 +97,6 @@ Czyść Zamknij Zamknij wybór - CODEC 2 włączony - Częstotliwość próbkowania CODEC2 - Szybkość kodowania Kurs: %1$s Kurs: N/A @@ -121,41 +105,18 @@ Oczekiwanie na poprawkę GPS, aby obliczyć odległość i kurs. Aby pokazać odległość i kurs, wymagane jest uprawnienie do lokalizacji. To urządzenie nie posiada czujnika kompasu. Kurs jest niedostępny. - Orientacja kompasu Kompas Szacowany obszar: \u00b1%1$s (\u00b1%2$s) Szacowany obszar: nieznana dokładność - Traktuj podwójne dotknięcie na obsługiwanych akcelerometrach jako naciśnięcie przycisku użytkownika. Kontroluje miganie LED na urządzeniu. Dla większości urządzeń będzie to sterować jednym z maksymalnie 4 diod LED, ładowarka i diody GPS nie są sterowane. - Czy oprócz wysyłania do MQTT i PhoneAPI, NeighborInfo powinny być przesłane przez LoRa? Niedostępny na kanale z domyślnym kluczem i nazwą. Wyślij lokalizację na kanale głównym, gdy przycisk użytkownika jest trzykrotnie kliknięty. Strefa czasowa dla dat na ekranie urządzenia i dzienniku. Użyj strefy czasowej telefonu - Automatycznie przewija się na następną stronę na ekranie jak karuzela, co określony interwał czasowy. - Nadpisz domyślny układ ekranu. - Odwróć ekran pionowo. - Nadpisz automatyczne wykrywanie ekranu OLED. - Jak długo ekran pozostaje włączony po naciśnięciu przycisku użytkownika lub odebraniu wiadomości. - Jednostki wyświetlane na ekranie urządzenia. - Wymagany jest akcelerometr na urządzeniu. - Częstotliwość robocza węzła jest obliczana na podstawie regionu, ustawień modemu i tego pola. Gdy wartość wynosi 0, slot jest automatycznie obliczany na podstawie nazwy kanału podstawowego i zmienia się z domyślnego slotu publicznego. Jeśli skonfigurowano prywatny kanał podstawowy i publiczny kanał dodatkowy, należy przywrócić domyślny slot publiczny. - Ustawia maksymalną liczbę przeskoków, domyślnie jest to 3. Zwiększenie liczby przeskoków powoduje również wzrost przeciążenia i należy stosować tę opcję ostrożnie. Komunikaty rozgłoszeniowe z 0 przeskokami nie otrzymają potwierdzeń ACK. Dostępne presety modemu, domyślnie Long Fast. Region, w którym będziesz używać urządzenia. - Włączenie połączenia Ethernet spowoduje wyłączenie bluetootha. Połączania TCP nie są dostępne na urządzeniach Apple. Włącz nadawanie pakietów poprzez UDP w lokalnej sieci. - Maksymalny odstęp czasu, jaki może upłynąć bez nadawania lokalizacji przez węzeł. - Minimalna zmiana odległości w metrach, którą należy uwzględnić w przypadku inteligentnego pozycjonowania. - Kiedy najszybciej pozycja zostanie zaktualizowana, jeśli minimalna odległość została osiągnięta. - Opcjonalne pola dołączane do danych lokalizacji. Im więcej pól, tym większy rozmiar pakietu, co wydłuża czas transmisji i zwiększa ryzyko jego utraty. - Jak często powinniśmy próbować uzyskać pozycję GPS (<10 sekund utrzymuje GPS włączony). - Uśpij wszystko na tak długo, jak to możliwe, w przypadku funkcji trackera i czujnika obejmie to również radio lora. Nie używaj tego ustawienia, jeśli chcesz korzystać z urządzenia z aplikacjami na telefon lub używasz urządzenia bez przycisków. - Klucz publiczny uprawniony do wysyłania wiadomości administracyjnych do tego węzła. - Pokaż na żywo logi debugowania przez połączenie szeregowe, podejrzyj i eksportuj logi węzła (bez informacji lokalizacyjnych) przez Bluetooth. - Urządzenie jest zarządzane przez administratora sieci mesh, użytkownik nie ma dostępu do żadnych ustawień urządzenia. Używane do tworzenia klucza współdzielonego ze zdalnym urządzeniem. - Konsola szeregowa przez Stream API. Konfiguracja Skonfiguruj uprawnienia powiadomień Połącz @@ -218,7 +179,6 @@ Szczegóły Czujnik detekcji Konfiguracja czujnika detekcji - Czujnik detekcji włączony Urządzenie Limit pamięci podręcznej urządzenia @@ -229,6 +189,7 @@ Punkt rosy Bezpośrednia wiadomość Bezpośrednie Wiadomości + Wyłączony Odrzuć Rozłącz Rozłączono @@ -238,39 +199,29 @@ Bezpośrednio Wiadomości Wybrane - Wyświetlacz Ustawienia wyświetlacza - Tryb wyświetlania - Wyświetlana jednostka Odległość - DNS Wyczyść wyszukiwanie MQTT Węzły Wykonano - Podwójne dotknięcie jako naciśnięcie przycisku Pobierz - Włącz echo + Edytuj 8 Godzin - Włącz tryb oszczędzania energii Włączony - - Szyfrowanie włączone Niezgodność klucza publicznego Klucz publiczny nie pasuje do zapisanego klucza. Możesz usunąć węzeł i pozwolić mu na ponowną wymianę kluczy, ale może to oznaczać poważniejszy problem z bezpieczeństwem. Skontaktuj się z użytkownikiem przez inny zaufany kanał, żeby sprawdzić, czy zmiana klucza była spowodowana przywróceniem ustawień fabrycznych lub innym celowym działaniem. Szyfrowanie klucza publicznego Metryki środowiskowe - Środowisko Błąd Osiągnięto limit nadawania. Nie można wysłać wiadomości w tej chwili, spróbuj później. Ustawienia Ethernet - Ethernet włączony Ethernet IP: Poproś o pozycję Wygasa @@ -279,15 +230,12 @@ Eksportuj wszystkie pakiety Zewnętrzne Powiadomienie Konfiguracja Zewnętrznego Powiadomienia - Powiadomienia zewnętrzne włączone Ustawienia fabryczne wystarczający Ulubiony Dodać węzeł '%1$s' do ulubionych? Usunąć węzeł '%1$s' z ulubionych? - Włącz filtrowanie - Ukryj wiadomości zawierające filtrowane słowa Filtr Filtry wiadomości Filtruj słowa @@ -327,29 +275,17 @@ Oczekiwanie na ponowne uruchomienie urządzenia w trybie OTA... Oczekiwanie na ponowne połączenie urządzenia... Wersja oprogramowania: %1$s - Stały PIN Położenie stałe - Odwróć ekran - Slot częstotliwości - Przyjazna nazwa Rezystancja gazu - Brama domyślna Generuj Kod QR Wyłączony dobry - GPIO Pin GPIO - Pin GPIO do monitorowania - GPS EN GPIO - GPS Rx GPIO - GPS Tx GPIO - Zielony Sprzęt Kierunek - Bicie serca Ukryj warstwę Ukryj hasło Skoków @@ -357,15 +293,12 @@ Zgadzam się. Przeczytałem powyższe informacje i rozumiem je. Wyrażam dobrowolną zgodę na niezaszyfrowaną transmisję danych mojego węzła za pośrednictwem protokołu MQTT Wiem, co robię. - Zegar I2S IAQ Jakość powietrza w pomieszczeniach (Indoor Air Quality) - wartość względna w skali IAQ mierzona czujnikiem BME680. Zakres wartości: 0–500. Znaczenia ikon - Zignoruj Dodać '%1$s' do listy ignorowanych? Ignoruj przychodzące - Zignoruj MQTT Usunąć '%1$s' z listy ignorowanych? Twoje urządzenie zostanie zrestartowane po tej zmianie. Import konfiguracji @@ -379,7 +312,6 @@ IP Adres IP: Port: - Tryb IPv4 Włącz wyjście JSON Ostatnia aktualizacja lokalizacji @@ -388,8 +320,6 @@ Szerokość geograficzna Dowiedz się więcej - LED bicia serca - Stan diody LED Ładowanie @@ -426,7 +356,6 @@ Zarządzaj warstwami map Mapa Sieci - Pojemność pamięci: %1$d MB\nUżycie pamięci: %2$d MB Zarządzanie pamięcią podręczną Aktualny rozmiar pamięci podręcznej %1$d mapy @@ -440,11 +369,8 @@ Menedżer map offline Usuwanie pamięci podręcznej SQL nie powiodło się, zobacz logcat Pamięć podręczna wyczyszczona dla %1$s - Raportowanie map Zgoda na udostępnianie niezaszyfrowanych danych węzła za pośrednictwem protokołu MQTT Włączając tę funkcję, użytkownik przyjmuje do wiadomości i wyraźnie wyraża zgodę na przesyłanie informacji o aktualnej lokalizacji geograficznej swojego urządzenia za pośrednictwem protokołu MQTT bez szyfrowania. Dane dotyczące lokalizacji mogą być wykorzystywane do takich celów, jak raportowanie na żywo na mapie, śledzenie urządzeń i powiązane funkcje telemetryczne. - Interwał raportowania map (sekundy) - Twój węzeł będzie okresowo wysyłał niezaszyfrowany pakiet raportu mapy do skonfigurowanego serwera MQTT, zawierający identyfikator, długą i krótką nazwę, przybliżoną lokalizację, model sprzętu, rolę, wersję oprogramowania układowego, region LoRa, ustawienia modemu i nazwę głównego kanału. Wybierz region do pobrania Rozpocznij pobieranie kierunek: %1$d° odległość: %2$s @@ -482,13 +408,11 @@ Zakolejkowane do wysłania Nieznany Wiadomości - Minimalny czas transmisji (sekundy) Presety Ustawienia modułu MQTT Konfiguracja MQTT - Włącz MQTT Połączony Rozłączono Musisz ustawić region! @@ -508,7 +432,6 @@ Przejdź wstecz Informacje o sąsiadze Konfiguracja Info o sąsiedzie - Włącz informacje o sąsiedzie Sieć Otrzymano nowy URL kanału Nowe wiadomości poniżej @@ -543,7 +466,6 @@ Przez ulubione Przez MQTT Zresetuj NodeDB - Interwał transmisji informacji o węźle Węzły Węzły w tej lokalizacji @@ -553,14 +475,12 @@ brak Nie połączono Notatki + Powiadomienia o otrzymaniu wiadomości Teraz - Serwer NTP - Ok dla MQTT OK - Typ ekranu OLED 24 Godzin 1 godzina @@ -571,14 +491,7 @@ Otwórz ustawienia Opcje Zorientuj na północ - - Wyjście buzzera (GPIO) - Czas trwania (w milisekundach) - Wyjście LED (GPIO) - Wyjście silnika wibracyjnego (GPIO) - Nadpisz cykl pracy - Tryb parowania Hasło Paxcounter @@ -594,29 +507,23 @@ %1$d godzin %1$d godzin + Pozycjonowanie Lokalizacja włączona - Flagi położenia Pozycjonowanie Pakiet lokalizacji - Zasilanie Konfiguracja zarządzania energią Metryki zasilania - Włącz moduł metryk zasilania - Wyświetlaj metryki zasilania na ekranie - Czas aktualizacji metryk zasilania Precyzyjna lokalizacja Język Domyślny systemu Ciśnienie Podstawowy - Klucz prywatny Podaj lokalizację telefonu do sieci PSK - Pin PTT (Push-To-Talk) Klucz publiczny Kod QR @@ -632,16 +539,6 @@ Test zasięgu Konfiguracja testu zasięgu Restart - - Tryb retransmisji - Przekazuje ponownie każdy odebrany pakiet, niezależnie od tego, czy został wysłany na nasz prywatny kanał, czy z innej sieci Mesh o tych samych parametrach radia. - To samo zachowanie co ALL, ale pomija dekodowanie pakietów i po prostu je retransmituje. Dostępne tylko w roli REPEATER. Ustawienie tego w innych rolach spowoduje zachowanie jak ALL. - Ignoruje niestandardowe pakiety (non-standard portnums) takie jak: TAK, RangeTest, PaxCounter, itp. Przekazuje dalej jedynie standardowe pakiety (standard portnums): NodeInfo, Text, Position, Telemetry oraz Routing. - Ignoruje odebrane pakiety z obcych sieci, podobnie jak LOCAL_ONLY, ale idzie o krok dalej, ignorując również pakiety z węzłów, które nie znajdują się jeszcze na liście znanych węzłów. - Ignoruje odebrane pakiety z obcych sieci Mesh, które są otwarte lub których nie można odszyfrować. Retransmituje wiadomość tylko na lokalnych kanałach primary / secondary. - Dozwolone wyłącznie dla ról SENSOR, TRACKER i TAK_TRACKER. Spowoduje to zablokowanie wszystkich retransmisji, podobnie jak rola CLIENT_MUTE. - Czerwony - Region Zdalne zarządzanie @@ -670,24 +567,12 @@ Rola urządzenia Klient - Urządzenie samodzielne lub sparowane z aplikacją. - Used for nodes that "only speak when spoken to" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption. Klient pasywny - Wyciszenie klienta - To samo, co klient, z wyjątkiem pakietów, które nie przeskakują przez ten węzeł, nie przyczynia się do routingu pakietów dla siatki. Repeater - Węzeł infrastruktury do rozszerzenia zasięgu sieci poprzez przekazywanie pakietów z minimalnym narzutem. Niewidoczny na liście węzłów. Router Router Klienta - Połączenie zarówno trybu ROUTER, jak i CLIENT. Nie dla urządzeń przenośnych. - Węzeł infrastruktury do rozszerzenia zasięgu sieci poprzez przekazywanie pakietów. Widoczny na liście węzłów. - Węzeł infrastruktury, który zawsze powtarza pakiety raz, ale tylko po wszystkich innych trybach, zapewniając dodatkowe pokrycie lokalnych klastrów. Widoczne na liście węzłów. Czujnik - Nadaje priorytetowo pakiety telemetryczne. TAK - Zoptymalizowany pod kątem komunikacji systemowej ATAK, redukuje nadmiarowe transmisje. - Umożliwia automatyczne transmisje TAK PLI i zmniejsza liczbę nadmiarowych transmisji. - Tracker - Do użytku z urządzeniami przeznaczonymi jako śledzenie GPS. Pakiety pozycyjne wysyłane z tego urządzenia będą miały wyższy priorytet, z nadawaniem pozycji co dwie minuty. Inteligentna transmisja pozycji będzie domyślnie wyłączona. - Główny temat Otrzymano negatywne potwierdzenie Brak trasy @@ -695,14 +580,11 @@ Upłynął limit czasu RSSI: Received Signal Strength Indicator - miara używana do określenia poziomu mocy odbieranej przez antenę. Wyższa wartość RSSI zazwyczaj oznacza silniejsze i bardziej stabilne połączenie. - Serwer rsyslog - Zapisz Zapisz Eksportuj pakiety zasięgu Skanowanie - Ekran włączony na Przewiń w dół Wtórny @@ -721,13 +603,8 @@ Wybrane Wybierz typ mapy Wyślij - Seryjny - Prędkość transmisji Konfiguracja seryjna - Włącz tryb serial - Tryb serial - Serwer Wybierz swój region ustawienia @@ -749,12 +626,9 @@ Wyświetlacz Pomiń - Inteligentne Pozycjonowanie SNR: Współczynnik sygnału do szumu (Signal-to-Noise Ratio) - miara stosowana w komunikacji do określania poziomu pożądanego sygnału w stosunku do poziomu szumu tła. W Meshtastic i innych systemach bezprzewodowych wyższy współczynnik SNR oznacza czystszy sygnał, który może zwiększyć niezawodność i jakość transmisji danych. Prędkość - SSID - Podsieć Obsługiwane Usuń Wycisz @@ -762,9 +636,6 @@ Serwer Wyłączony - Niebieski - Zielony - Czerwony Telemetria Konfiguracja telemetrii Motyw @@ -772,10 +643,8 @@ Jasny Domyślne ustawienie systemowe Czas - Strefa czasowa Limit czasu Znacznik czasu - Włącz TLS Pokaż moją pozycję Śledzenie trasy @@ -800,7 +669,6 @@ Moduł Włączony - Nadaj przez LoRa BLE LoRa @@ -808,9 +676,9 @@ 24H 48 Godzin 2W - Nadawanie włączone - Moc nadawania Typ + dBm + m Domyślny systemu @@ -822,26 +690,17 @@ Nie przyjmuje wiadomości Niemonitorowany lub infrastruktura Nierozpoznany - Częstotliwość aktualizacji (w sekundach) Czas pracy URL - - Użyj formatu 12-godzinnego - Użyj I2S jako buzzer - Użyj trybu INPUT_PULLUP - Użyj predefiniowanych ustawień - Użyj buzzer PWM Użytkownik Konfiguracja użytkownika ID użytkownika Informacje o użytkowniku - Nazwa użytkownika Przez MQTT Pokaż na mapie Napięcie - Wybudź przy dotknięciu lub ruchu Uwaga Usuń punkt nawigacji? Edytuj punkt nawigacji diff --git a/core/resources/src/commonMain/composeResources/values-pt-rBR/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-pt-rBR/schema_strings.xml new file mode 100644 index 0000000000..c8857465fa --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-pt-rBR/schema_strings.xml @@ -0,0 +1,148 @@ + + + + + Azul + Atual + Verde + Estado do LED + Vermelho + Padrão + Taxa de amostragem CODEC2 + CODEC 2 ativado + MISO I2S + CLK I2S + MOSI I2S + CS I2S + Pino de PTT + Bluetooth habilitado + Modo de pareamento + Gerar evento de entrada rodando no sentido anti-horário + Gerar evento de entrada rodando no sentido horário + Gerar evento de entrada ao pressionar + Pino GPIO para porta A do codificador rotativo + Pino GPIO para porta B do codificador rotativo + Pino GPIO para codificador rotativo porta Press + Voltar + Cancelar + Nenhum + Codificador rotativo #1 ativado + Enviar sino + Entrada Cima/Baixo/Selecionar ativada + Tipo de gatilho de deteção + Sensor de Detecção ativado + Pino GPIO para monitorar + Nome amigável + Enviar sino com mensagem de alerta + Usar o modo INPUT_PULLUP + Tratar toque duplo nos acelerômetros suportados enquanto um botão pressionado pelo usuário. + Todos + O mesmo que o comportamento de TODOS, mas ignora a decodificação de pacotes e simplesmente os retransmite. Apenas disponível no papel de Repetidor. Configurar isso em qualquer outra função resultará em comportamento como TODOS. + Ignora mensagens observadas de malhas estrangeiras que estão abertas ou aquelas que não pode descriptografar. Apenas retransmite mensagem nos nós de canais primários / secundários. + Nenhum + Somente permitido para os papéis SENSOR, TRACKER e TAK_TRACKER, isso irá inibir todas as retransmissões, como do papel CLIENT_MUTE. + Dispositivo que só transmite conforme necessário para economizar energia ou se manter em segredo. + Dispositivo que não retransmite pacotes de outros dispositivos. + Transmite o local como mensagem para o canal padrão regularmente para ajudar na recuperação do dispositivo. + Transmita pacotes de telemetria como prioridade. + Otimizado para a comunicação do sistema ATAK, reduz as transmissões de rotina. + Habilita transmissões automáticas TAK PLI e reduz as transmissões rotineiras. + Transmita pacotes de posição do GPS como prioridade. + Orientação da bússola + Modo da tela + Inverter tela + Unidades de exibição + Usar formato de relógio 12h + Quando ativado, o dispositivo exibirá o tempo em formato de 12 horas na tela. + LED de saída habilitado alto + LED de alerta de sino + Campainha de alerta + Vibração da campainha de alerta + LED de mensagem de alerta + Campainha de mensagem de alerta + Vibração de mensagem de alerta + Notificação Externa habilitada + LED de Saída (GPIO) + Saída Campainha (GPIO) + Vibra de saída (GPIO) + Usar I2S como campainha + Usar uma campainha PWM + Largura da banda + Ignorar MQTT + Ignorar ciclo de trabalho + Ventilador do PA desativado + Região + Mensagem + Endereço + MQTT habilitado + Criptografia ativada + Senha + Proxy para cliente ativado + Tópico principal + Nome de usuário + Informações do Vizinho ativado + Transmitir por LoRa + Se além de enviá-lo para MQTT e PhoneAPI, nosso NeighborInfo deve ser transmitido por LoRa. Não disponível em um canal com chave e nome padrão. + Modo IPv4 + Ethernet ativado + Gateway + IP + Subnet + Servidor NTP + Nenhum + Servidor rsyslog + Senha + SSID + Limite de RSSI BLE (o padrão é -80) + Contador de Pessoas ativado + Altitude + Data e hora + Alterar proporção do multiplicador ADC + Ativar modo de economia de energia + Teste de distância ativado + Salvar .CSV no armazenamento (apenas ESP32) + Chave do Administrador + API de logs de depuração ativada + Modo Administrado + Chave Privada + Chave Publica + Console serial + Taxa de transmissão série + Taxa de transmissão série + Eco habilitado + Serial ativado + Modo de série + Padrão + Padrão + Tempo esgotado + Batimento + Histórico de retorno máximo + Janela de retorno do histórico + Servidor + Número de registros + Função + Azul + Verde + Vermelho + Módulo de métricas de qualidade do ar habilitado + Métricas de Ambiente usam Fahrenheit + Módulo de métricas do ambiente ativado + Métricas de ambiente na tela habilitado + Módulo de métricas de energia habilitado + Métricas de ambiente na tela habilitado + diff --git a/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml b/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml index 4ae8a84466..783c686e4f 100644 --- a/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -17,17 +17,18 @@ --> + Umidade %1$s: %2$s Mensagem de %1$s %2$s %1$s de distância favorito - A %1$d salto de distância Visto pela última vez há %1$s desconectado conectado função %1$s sinal %1$s Saltar %1$d a %2$d nós + Temperatura Sobre Aceitar Agradecimentos @@ -43,7 +44,6 @@ Alternar entre texto original e traduzido Traduzir mensagem Ações - Alterar proporção do multiplicador ADC Adicionar Adicionar uma nota privada… @@ -52,23 +52,14 @@ Adicionar Adicionar dispositivo manualmente… Endereço - Chave do Administrador Chaves do Administrador Administração Avançado Avançado Ícone da qualidade do ar - Módulo de métricas de qualidade do ar habilitado Percentagem do tempo de ar utilizado na última hora para transmissões. - - Campainha de alerta - LED de alerta de sino Caractere de Alerta! - Vibração da campainha de alerta - Campainha de mensagem de alerta - LED de mensagem de alerta - Vibração de mensagem de alerta Todos Permitir fonte de entrada Permitir acesso indefinido ao pino @@ -97,16 +88,11 @@ Pinos disponíveis Voltar Ruim - - Largura da banda Bateria Endereço I2C da bateria INA_2XX - Limite de RSSI BLE (o padrão é -80) - Azul Bluetooth Configuração do Bluetooth - Bluetooth habilitado Bluetooth Configurações Calculando… @@ -129,7 +115,6 @@ Canal 7 Canal 8 Este link de canal é inválido e não pode ser usado - Canal Nome do canal Canais @@ -147,17 +132,12 @@ Notificação de cliente Fechar Fechar seleção - CODEC 2 ativado - Taxa de amostragem CODEC2 Comunique-se off-grid com seus amigos e comunidades sem serviço de celular. Norte da bússola no topo - Orientação da bússola Chaves comprometidas foram detectadas, selecione OK para regenerar. - Tratar toque duplo nos acelerômetros suportados enquanto um botão pressionado pelo usuário. Controla o LED piscando no dispositivo. Para a maioria dos dispositivos, isto controlará um dos até 4 LEDs, os LEDs do carregador e GPS não são controláveis. - Se além de enviá-lo para MQTT e PhoneAPI, nosso NeighborInfo deve ser transmitido por LoRa. Não disponível em um canal com chave e nome padrão. Configurar Alertas Críticos Configurar Permissões de Localização Configurar permissões de notificação @@ -186,7 +166,6 @@ Limpar todos os filtros Filtro incluído Filtros - API de logs de depuração ativada Exportar Logs Painel de depuração Limpar busca @@ -211,8 +190,6 @@ Detalhes Sensor de Detecção Configuração do Sensor de Detecção - Sensor de Detecção ativado - Tipo de gatilho de deteção Dispositivo Dispositivo em suspensão (sleep) @@ -226,11 +203,7 @@ Direto Mensagens Disco Livre %1$d - Tela - Modo da tela - Quando ativado, o dispositivo exibirá o tempo em formato de 12 horas na tela. - Unidades de exibição Distância Filtros de Distância @@ -244,32 +217,23 @@ Nós Concluído Baixar + Dinâmico Configure facilmente redes de malha privada para uma comunicação segura e confiável em áreas remotas. - Eco habilitado Editar 8 Horas - Ativar modo de economia de energia - - Criptografia ativada Chave pública não confere Criptografia de Chave Pública - Ambiente - Módulo de métricas do ambiente ativado - Métricas de ambiente na tela habilitado - Métricas de Ambiente usam Fahrenheit Erro Limite de capacidade atingido. Não é possível enviar mensagens no momento. Por favor, tente novamente mais tarde. - Ethernet ativado Trocar posições Expira em Exportar configurações Notificação Externa Configuração de Notificações Externas - Notificação Externa habilitada Redefinição de fábrica Média Meshtastic %1$s @@ -286,52 +250,30 @@ Concluído Atualização falhou Versão do firmware: %1$s - PIN fixo - Inverter tela Memória Livre Freq - Nome amigável Resistência ao gás - Gateway - Gerar evento de entrada rodando no sentido anti-horário - Gerar evento de entrada rodando no sentido horário - Gerar evento de entrada ao pressionar Vamos começar Bom - Pino GPIO - Pino GPIO para porta A do codificador rotativo - Pino GPIO para porta B do codificador rotativo - Pino GPIO para codificador rotativo porta Press - Pino GPIO para monitorar - Verde Hardware Modelo de hardware Direção - Batimento Ocultar Camada Ocultar senha - Histórico de retorno máximo - Janela de retorno do histórico Qtd de saltos Host Métricas do Host Eu concordo. Eu li e entendo o acima. Eu concordo voluntariamente com a transmissão não criptografada dos dados do meu nó via MQTT. Eu sei o que estou fazendo. - CLK I2S - MOSI I2S - MISO I2S - CS I2S IAQ (Qualidade do ar interior) valor relativo da escala IAQ medido pelo Bosch BME680. Intervalo de Valor de 0–500. - Ignorar Adicionar '%1$s' na lista de ignorados? Ignorar entrada - Ignorar MQTT Remover '%1$s' da lista de ignorados? Importar configurações @@ -348,7 +290,6 @@ IP Endereço IP: Porta: - Modo IPv4 Saída JSON ativada Atualização da última posição @@ -356,7 +297,6 @@ Último estável Latitude - Estado do LED Canal de administração antigo Ativar esta opção desativa a criptografia e não é compatível com a rede padrão do Meshtastic. @@ -379,11 +319,9 @@ Lux Gerenciar Fontes de Bloco Personalizados Gerenciar Camadas do Mapa - Modo Administrado Pedidos de posição obrigatoriamente manual Mapa Mesh - Capacidade do Cache: %1$d MB\nCache Utilizado: %2$d MB Gerenciador de cache Tamanho atual do cache %1$d blocos @@ -398,11 +336,8 @@ Gerenciador offline Falha na remoção do cache SQL, consulte logcat para obter detalhes Cache SQL removido para %1$s - Relatório de mapa Consentir para compartilhar dados de nó não criptografados via MQTT Ao ativar este recurso, você reconhece e concorda expressamente com a transmissão da localização geográfica em tempo real do seu dispositivo pelo protocolo MQTT sem criptografia. Esses dados de localização podem ser usados para propósitos como relatório de mapa ao vivo, rastreamento do dispositivo e funções de telemetria relacionadas. - Intervalo de relatório de mapa (segundos) - Seu nó enviará periodicamente um pacote de relatório de mapa não criptografado para o servidor MQTT configurado, incluindo, id, nome longo e curto, localização aproximada, modelo de hardware, função de firmware, região de LoRa, predefinição de modem e nome de canal primário. Selecione a região para download Iniciar download direção: %1$d° distância: %2$s @@ -441,13 +376,11 @@ Programado para envio Desconhecido Mensagens - Transmissão mínima (segundos) Configuração de módulos Módulos desbloqueados MQTT Configurações MQTT - MQTT habilitado Conectado Desconectado Você deve informar uma região! @@ -457,14 +390,12 @@ 8 horas Sempre Desativar notificações - Tempo limite do Nag (segundos) Nome Nome não pode estar vazio. Voltar Navegar Em Informações do Vizinho Configuração de Inform. do Vizinho - Informações do Vizinho ativado Rede Novo link de canal recebido Novo Nó Visto: %1$s @@ -499,6 +430,7 @@ Nenhum (desabilitado) Nenhum Não conectado + Meshtastic usa notificações para mantê-lo atualizado sobre novas mensagens e outros eventos importantes. Você pode atualizar suas permissões de notificação a qualquer momento nas configurações. Notificações para canais e mensagens diretas. @@ -506,8 +438,6 @@ Notificações para nós recém-descobertos. Notificações no recibo de alerta/sino Notificações no recibo de mensagem - Servidor NTP - Número de registros Ok @@ -516,39 +446,27 @@ Somente Favoritos Abrir configurações - - Saída Campainha (GPIO) - Duração da Saída (milissegundos) - LED de saída habilitado alto - LED de Saída (GPIO) - Vibra de saída (GPIO) Menu Overflow Substituir porta série do console - Ignorar ciclo de trabalho - Ventilador do PA desativado - Modo de pareamento Senha PAX Medidor de Fluxo de Pessoas Configuração do Contador de Pessoas - Contador de Pessoas ativado Meshtastic precisa das permissões de "Dispositivos próximos" habilitadas para localizar e conectar a dispositivos via Bluetooth. Você pode desativar quando não estiver em uso. Localização do Telefone Meshtastic usa a localização do seu telefone para habilitar vários recursos. Você pode atualizar as permissões de localização a qualquer momento a partir das configurações. + Posição Definir a partir da localização atual do telefone Posição ativada Posição - Energia Configuração de Energia - Módulo de métricas de energia habilitado - Métricas de ambiente na tela habilitado Localização precisa Idioma Padrão do sistema @@ -557,12 +475,9 @@ Primário Transmissão periódica da posição e telemetria - Chave Privada Fornecer localização para mesh O nome do provedor existe. - Proxy para cliente ativado PSK - Pino de PTT Chave Publica Chave Pública Mudou Código QR @@ -578,21 +493,11 @@ Configurações do dispositivo Teste de Alcance Configuração de Teste de Distância - Teste de distância ativado Reagir Reiniciar - - Retransmita qualquer mensagem observada, se estivesse em nosso canal privado ou de outra malha com os mesmos parâmetros de lora. - O mesmo que o comportamento de TODOS, mas ignora a decodificação de pacotes e simplesmente os retransmite. Apenas disponível no papel de Repetidor. Configurar isso em qualquer outra função resultará em comportamento como TODOS. - Ignora pacotes de portnums não padrão como: TAK, RangeTest, PaxCounter, etc. Apenas retransmite pacotes com portnums padrão: NodeInfo, Text, Position, Telemetry, and Routing. - Ignora mensagens observadas de malhas estrangeiras como APENAS LOCAL, e vai ainda mais longe ignorando também mensagens de nós que não estão na lista conhecida do nó. - Ignora mensagens observadas de malhas estrangeiras que estão abertas ou aquelas que não pode descriptografar. Apenas retransmite mensagem nos nós de canais primários / secundários. - Somente permitido para os papéis SENSOR, TRACKER e TAK_TRACKER, isso irá inibir todas as retransmissões, como do papel CLIENT_MUTE. Dispositivos de Rede Recentes - Vermelho Tem certeza de que deseja regenerar sua Chave Privada?\n\nnós que podem ter trocado chaves anteriormente com este nó precisará remover aquele nó e re-trocar chaves a fim de retomar uma comunicação segura. Regenerar a chave privada - Região Remoto Administração Remota @@ -614,19 +519,6 @@ Redefinir para configurações originais Toque - Aplicativo conectado ou é um dispositivo autônomo de mensagem. - Dispositivo que só transmite conforme necessário para economizar energia ou se manter em segredo. - Dispositivo que não retransmite pacotes de outros dispositivos. - Nó de infraestrutura para estender a cobertura da rede repassando mensagens com sobrecarga mínima. Não visível na lista de nós. - Combinação de ROUTER e CLIENT. Incompatível com dispositivos móveis. - Nó de infraestrutura para estender a cobertura da rede repassando mensagens. Visível na lista de nós. - Nó de infraestrutura que sempre retransmitirá pacotes somente uma vez depois de todos os outros modos, garantindo cobertura adicional para clusters locais. Visível na lista de nós. - Transmita pacotes de telemetria como prioridade. - Otimizado para a comunicação do sistema ATAK, reduz as transmissões de rotina. - Habilita transmissões automáticas TAK PLI e reduz as transmissões rotineiras. - Transmita pacotes de posição do GPS como prioridade. - Tópico principal - Codificador rotativo #1 ativado Recebi uma negativa de reconhecimento Sem rota @@ -634,12 +526,9 @@ Tempo esgotado RSSI Indicador de Força de Sinal Recebido, uma medida usada para determinar o nível de potência que está sendo recebida pela antena. Um valor maior de RSSI geralmente indica uma conexão mais forte e mais estável. - Servidor rsyslog Sats - Salvar Salvar - Salvar .CSV no armazenamento (apenas ESP32) Escanear Rolar para o final @@ -665,17 +554,8 @@ Selecionar tudo Tipo de Mapa Selecionado Enviar - Enviar sino - Enviar sino com mensagem de alerta - Intervalo de mensagem do remetente (segundos) - Serial - Taxa de transmissão série Configuração Serial - Console serial - Serial ativado - Modo de série - Servidor Defina sua região configurações @@ -701,10 +581,7 @@ SNR Relação sinal-para-ruído, uma medida utilizada nas comunicações para quantificar o nível de um sinal desejado para o nível de ruído de fundo. Na Meshtastic e outros sistemas sem fios, uma SNR maior indica um sinal mais claro que pode melhorar a confiabilidade e a qualidade da transmissão de dados. Velocidade - SSID - Transmissão de estado (segundos) Fique Conectado em Qualquer Lugar - Subnet Suportado Apoiado pela Comunidade Meshtastic Excluir @@ -712,9 +589,6 @@ Desmutar Servidor - Azul - Verde - Vermelho Telemetria Configuração de Telemetria Tema @@ -724,7 +598,6 @@ Hora Tempo esgotado Data e hora - TLS ativado Habilitar minha posição Traçar rota @@ -739,7 +612,6 @@ ponto de rastreamento - Transmitir por LoRa LoRa MQTT @@ -759,25 +631,17 @@ Não monitorizado ou infraestrutura Desmutar Desconhecido - Entrada Cima/Baixo/Selecionar ativada - Intervalo de atualização (segundos) Uptime URL A URL não pode estar vazia. A URL deve conter espaços reservados. Modelo de URL - - Usar formato de relógio 12h - Usar I2S como campainha - Usar o modo INPUT_PULLUP - Usar uma campainha PWM Usuário Configuração do Usuário ID do usuário String de Usuário - Nome de usuário Luz UV via MQTT Ver Lançamento diff --git a/core/resources/src/commonMain/composeResources/values-pt/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-pt/schema_strings.xml new file mode 100644 index 0000000000..5380b9d8b6 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-pt/schema_strings.xml @@ -0,0 +1,167 @@ + + + + + Azul + Atual + Verde + Estado do LED + Vermelho + Predefinição + Taxa de amostragem CODEC2 + CODEC 2 ativado + Saída de dados I2S + Relógio I2S + Entrada de dados I2S + Selecionar palavra I2S + Pin de PTT + Bluetooth ativado + Modo de emparelhamento + Gerar evento de entrada rodando no sentido oposto ao horário + Gerar evento de entrada rodando no sentido horário + Gerar evento de entrada ao pressionar + Pin GPIO para porta A do codificador rotativo + Pin GPIO para porta B do codificador rotativo + Cancelar + Nenhum + Ativar Codificador rotativo #1 + Enviar sino + Entrada Cima/Baixo/Selecionar ativa + Tipo de gatilho de deteção + Sensor de deteção ativado + Pin GPIO para monitorizar + Nome amigável + Enviar sino com mensagem de alerta + Usar o modo INPUT_PULLUP + Tratar toques duplos em acelerómetros suportados como pressionar um botão. + Tudo + Modo indêntico ao ALL, mas apenas retransmite os dados sem os descodificar. Apenas disponível em modo Repeater. Esta opção em qualquer outro modo resulta em comportamento igual ao ALL. + Ignora mensagens observadas de malhas estrangeiras que estão abertas ou aquelas que não pode desencriptar. Apenas retransmite mensagem nos canais primários / secundários locais. + Nenhum + Permitido apenas para SENSOR, TRACKER e TAK_TRACKER, isto irá desativar todas as retransmissões, como o papel CLIENT_MUTE. + Cliente + Cliente oculto + Dispositivo que só transmite quando necessário para economizar energia ou anonimidade. + Cliente silenciado + Dispositivo que não encaminha mensagens de outros dispositivos. + Perdidos e Achados + Transmite regularmente a localização como uma mensagem para o canal default, para auxiliar na recuperação do dispositivo. + Repetidor + Roteador + Cliente Roteador + Sensor + Transmite dados de telemetria como prioridade. + TAK — ‘Kit’ de Consciencialização da Equipa + Otimizado para comunicação do sistema ATAK, reduz as transmissões de rotina. + Permite transmissões automáticas do TAK PLI e reduz as transmissões de rotina. + Monitor + Transmite dados de posições GPS como prioridade. + Orientação da bússola + Modo de visualização + Inverter ecrã + Unidade de visualização + Usar formato de relógio 12h + Quando ativado, o dispositivo exibirá o tempo em formato de 12 horas no ecrã. + LED de saída ativo alto + LED de alerta de sino + Som de alerta de sino + Vibração de alerta de sino + LED de mensagem de alerta + Som de mensagem de alerta + Vibração de mensagem de alerta + Ativar notificações externas + LED de Saída (GPIO) + Buzzer de saída (GPIO) + Vibra de saída (GPIO) + Usar I2S como buzzer + Usar um buzzer PWM + Largura de banda + A frequência de operação do seu nó é calculada com base na região, na predefinição do modem e neste campo. Quando o valor é 0, o slot é calculado automaticamente com base no nome do canal primário e será diferente do slot público predefinido. Volte para o slot público predefinido se forem configurados canais primário privado e secundário público. + Define o número máximo de saltos; o valor predefinido é 3. Aumentar o número de saltos também aumenta a congestão e deve ser usado com precaução. Mensagens de broadcast com 0 saltos não receberão ACKs. + Ignorar MQTT + Ignorar ciclo de trabalho + Região + Mensagem + Endereço + MQTT ativo + Encriptação ativada + Palavra-passe + Enviar através do cliente + Tópico principal + Utilizador + Enviar informações de vizinhos + Enviar por LoRa + Além de enviar para MQTT e PhoneAPI, a vizinhança deve ser transmitida através da LoRa. Não disponível em canais com chave e nome padrão. + Intervalo de Atualização + Modo IPv4 + Ativar a transmissão de pacotes via UDP através da rede local. + Ethernet ativada + Ativar a Ethernet irá desativar a ligação Bluetooth à aplicação. As ligações TCP do nó não estão disponíveis em dispositivos Apple. + Gateway + IP + Subnet + Servidor NTP + Nenhum + servidor rsyslog + Ativar o WiFi irá desativar a ligação Bluetooth à aplicação. + Palavra-passe + SSID + Limite de RSSI BLE (o padrão é -80) + Ativar contador de pessoas + Intervalo de Atualização + Distância Mínima + Intervalo Mínimo + Posição fixa + Intervalo de Atualização + Intervalo de difusão + O intervalo máximo que pode decorrer sem que um nó transmita a sua posição. + Posição Inteligente + Altitude + Data e hora + Alterar rácio do multiplicador ADC + Ativar modo de poupança de energia + Ativar Teste de alcance + Guardar .CSV no armazenamento (apenas ESP32) + Chave do Administrador + API de histórico de depuração ativada + Modo Administrado + Chave privada + Chave pública + Consola de série + Taxa de transmissão série + Taxa de transmissão série + Eco ativado + Série ativada + Modo de série + Predefinição + Predefinição + Timeout + Batimento + Servidor + Número de registos + Papel + Azul + Verde + Vermelho + Módulo de métricas de qualidade do ar ativado + Métricas de Ambiente usam Fahrenheit + Módulo de métricas de ambiente ativado + Mostrar métricas de ambiente no ecrã + Módulo de métricas de energia ativado + Mostrar métricas de energia no ecrã + diff --git a/core/resources/src/commonMain/composeResources/values-pt/strings.xml b/core/resources/src/commonMain/composeResources/values-pt/strings.xml index 956aa9794c..5269d68bbc 100644 --- a/core/resources/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pt/strings.xml @@ -17,29 +17,21 @@ --> + Humidade + Temperatura Sobre Aceitar Excluir mensagem Ações - Alterar rácio do multiplicador ADC Adicionar Adicionar Endereço - Chave do Administrador Administração - Módulo de métricas de qualidade do ar ativado Percentagem do tempo de transmissão utilizado na última hora. - - Som de alerta de sino - LED de alerta de sino Símbolo de alerta - Vibração de alerta de sino - Som de mensagem de alerta - LED de mensagem de alerta - Vibração de mensagem de alerta Tudo Permitir fonte de entrada Permitir acesso indefinido ao pin @@ -58,20 +50,14 @@ Configurações de áudio Pins disponíveis Mau - - Largura de banda Bateria Endereço I2C da bateria INA_2XX - Limite de RSSI BLE (o padrão é -80) - Azul Bluetooth Configuração de Bluetooth - Bluetooth ativado Configuração Bluetooth Definições - Intervalo de difusão Calculando… Permissão da câmera Cancelar @@ -91,7 +77,6 @@ Canal 7 Canal 8 O Link Deste Canal é inválido e não pode ser usado - Canal Nome do Canal Canal @@ -100,20 +85,11 @@ Limpar Fechar - CODEC 2 ativado - Taxa de amostragem CODEC2 Norte da bússola no topo - Orientação da bússola - Tratar toques duplos em acelerómetros suportados como pressionar um botão. Controla o piscar do LED no dispositivo. Para a maioria dos dispositivos, isto controla um dos até 4 LEDs, os do carregador e GPS não são controláveis. - Além de enviar para MQTT e PhoneAPI, a vizinhança deve ser transmitida através da LoRa. Não disponível em canais com chave e nome padrão. - A frequência de operação do seu nó é calculada com base na região, na predefinição do modem e neste campo. Quando o valor é 0, o slot é calculado automaticamente com base no nome do canal primário e será diferente do slot público predefinido. Volte para o slot público predefinido se forem configurados canais primário privado e secundário público. - Define o número máximo de saltos; o valor predefinido é 3. Aumentar o número de saltos também aumenta a congestão e deve ser usado com precaução. Mensagens de broadcast com 0 saltos não receberão ACKs. - Ativar a Ethernet irá desativar a ligação Bluetooth à aplicação. As ligações TCP do nó não estão disponíveis em dispositivos Apple. Ativar a transmissão de pacotes via UDP através da rede local. - O intervalo máximo que pode decorrer sem que um nó transmita a sua posição. Configuração Ligar Ligado @@ -124,7 +100,6 @@ Atual Depuração - API de histórico de depuração ativada Painel de depuração Predefinição @@ -143,8 +118,6 @@ Detalhes Sensor de deteção Configuração do Sensor de Deteção - Sensor de deteção ativado - Tipo de gatilho de deteção Dispositivo GPS do Dispositivo @@ -156,41 +129,28 @@ Direto Mensagens - Ecrã - Modo de visualização - Quando ativado, o dispositivo exibirá o tempo em formato de 12 horas no ecrã. - Unidade de visualização Distância Ligações MQTT Nodes + Dinâmico - Eco ativado Editar 8 Horas - Ativar modo de poupança de energia - - Encriptação ativada Incompatibilidade de chave pública Criptografia de chave pública - Ambiente - Módulo de métricas de ambiente ativado - Mostrar métricas de ambiente no ecrã - Métricas de Ambiente usam Fahrenheit Erro Limite do ciclo de trabalho atingido. Não é possível enviar mensagens no momento. Tente novamente mais tarde. - Ethernet ativada Intercâmbio de posições Exportar configuração Notificação externa Configuração de Notificação Externa - Ativar notificações externas Redefinição de fábrica Razoável Favoritos @@ -203,45 +163,26 @@ Versão de firmware do rádio muito antiga para comunicar com este aplicativo. Para mais informações consultar Nosso guia de instalação de firmware. Necessário atualização de firmware. Atualização falhou - PIN fixo Posição fixa - Inverter ecrã Memória livre Freq - Nome amigável - Gateway - Gerar evento de entrada rodando no sentido oposto ao horário - Gerar evento de entrada rodando no sentido horário - Gerar evento de entrada ao pressionar Bom - Pin GPIO - Pin GPIO para porta A do codificador rotativo - Pin GPIO para porta B do codificador rotativo - Pin GPIO para monitorizar - Verde Hardware Modelo de hardware Direção - Batimento Ocultar palavra-passe Saltos Estou de acordo. Eu li e entendi a informação acima. Eu concordo voluntariamente com a transmissão não criptografada dos dados do meu node via MQTT Eu sei o que estou a fazer. - Relógio I2S - Entrada de dados I2S - Saída de dados I2S - Selecionar palavra I2S Qualidade do Ar Interior (Qualidade do ar interior) valor relativo da escala IAQ conforme medida por Bosch BME680. Entre 0–500. - Ignorar Adicionar '%1$s' para a lista de ignorados? Ignorar entrada - Ignorar MQTT Remover '%1$s' de lista dos ignorados? Importar configuração @@ -255,12 +196,10 @@ IP Endereço IP: Porta: - Modo IPv4 Saída JSON ativada Latitude - Estado do LED Canal de administração antigo Ativar esta opção desativa a encriptação e não é compatível com a rede Meshtastic normal. @@ -278,10 +217,8 @@ O node %1$s tem a bateria fraca (%2$d%) Bateria fraca: %1$s Lux - Modo Administrado Pedidos de posição obrigatoriamente manuais - Capacidade do Cache: %1$d MB\nCache Utilizado: %2$d MB Gerenciador de cache Tamanho atual do cache %1$d blocos @@ -295,11 +232,8 @@ Gerenciador offline Falha na remoção do cache SQL, consulte logcat para obter detalhes Cache SQL removido para %1$s - Enviar para o mapa Consentimento para partilhar dados não criptografados do node via MQTT Ao ativar este recurso, reconhece e consente expressamente com a transmissão da localização geográfica em tempo real do seu dispositivo pelo protocolo MQTT sem criptografia. Esses dados de localização podem ser usados para propósitos como relatório de mapas ao vivo, rastreamento do dispositivo e funções de telemetria relacionadas. - Intervalo de envio (segundos) - O seu node enviará periodicamente um pacote de relatório de mapa não criptografado para o servidor MQTT configurado, isso inclui id, nome longo e curto nome. localização aproximada, modelo de hardware, papel, versão do firmware, região LoRa, predefinição de modem e nome do canal primário. Selecione a região para download Iniciar download direção: %1$d° distância: %2$s @@ -325,12 +259,10 @@ Mensagem Na fila de envio Mensagens - Transmissão mínima (segundos) Configurações dos módulos MQTT Configuração MQTT - MQTT ativo Ligado Desconectado Você deve informar uma região! @@ -340,12 +272,10 @@ 8 horas Sempre Silenciar notificações - Tempo limite a incomodar (segundos) Nome Retroceder Informações da vizinhança Configuração de informações dos vizinhos - Enviar informações de vizinhos Rede Novo Link Recebido do Canal @@ -377,43 +307,31 @@ Nenhum (desabilitado) Nenhum Desligado + Notificações no recibo de alerta/sino Notificações no recibo de mensagem - Servidor NTP - Número de registos Okay 24 Horas 1sem - - Buzzer de saída (GPIO) - Duração da Saída (milissegundos) - LED de saída ativo alto - LED de Saída (GPIO) - Vibra de saída (GPIO) Substituir porta série do console - Ignorar ciclo de trabalho - Modo de emparelhamento Palavra-passe Contador de pessoas Configuração do contador de pessoas - Ativar contador de pessoas + Posição Posição ativada Posição - Energia Configuração de Energia - Módulo de métricas de energia ativado - Mostrar métricas de energia no ecrã Idioma Padrão do sistema Pressionar e arrastar para reordenar @@ -421,11 +339,8 @@ Principal Difusão periódica da posição e telemetria - Chave privada Fornecer localização para mesh - Enviar através do cliente PSK - Pin de PTT Chave pública Chave Pública Mudou Código QR @@ -439,17 +354,7 @@ Configurações do dispositivo Teste de Alcance Configuração de Teste de Alcance - Ativar Teste de alcance Reiniciar - - Se estiver no nosso canal privado ou de outra rede com os mesmos parâmetros LoRa, retransmite qualquer mensagem observada. - Modo indêntico ao ALL, mas apenas retransmite os dados sem os descodificar. Apenas disponível em modo Repeater. Esta opção em qualquer outro modo resulta em comportamento igual ao ALL. - Ignora pacotes de portas não padrão, tais como: TAK, RangeTest, PaxCounter, etc. Apenas retransmite pacotes com portas padrão: NodeInfo, Texto, Posição, Telemetria e Roteamento. - Ignora mensagens observadas de malhas estrangeiras, como APENAS LOCAL, mas leva mais longe ignorando também mensagens de nodes que não já estão na lista conhecida do node. - Ignora mensagens observadas de malhas estrangeiras que estão abertas ou aquelas que não pode desencriptar. Apenas retransmite mensagem nos canais primários / secundários locais. - Permitido apenas para SENSOR, TRACKER e TAK_TRACKER, isto irá desativar todas as retransmissões, como o papel CLIENT_MUTE. - Vermelho - Região Administração Remota Equipamento remoto @@ -468,28 +373,15 @@ Toque Cliente - Ligado por app, ou dispositivo autónomo de mensagens. Cliente oculto - Dispositivo que só transmite quando necessário para economizar energia ou anonimidade. Cliente silenciado - Dispositivo que não encaminha mensagens de outros dispositivos. Perdidos e Achados Repetidor - Node de infraestrutura para estender a cobertura da rede retransmitindo mensagens com overhead mínimo. Não visível na lista de nodes. Roteador Cliente Roteador - Combinação de ROUTER e CLIENT. Não indicado para dispositivos móveis. - Node de infraestrutura que retransmite mensagens para estender a cobertura da rede (Router). Visível na lista de nodes. - Node de infraestrutura que vai sempre retransmitir dados uma vez, mas apenas após todos os outros modos, garantindo cobertura adicional para grupos locais. Visível na lista de nós. Sensor - Transmite dados de telemetria como prioridade. TAK — ‘Kit’ de Consciencialização da Equipa - Otimizado para comunicação do sistema ATAK, reduz as transmissões de rotina. - Permite transmissões automáticas do TAK PLI e reduz as transmissões de rotina. Monitor - Transmite dados de posições GPS como prioridade. - Tópico principal - Ativar Codificador rotativo #1 Recebida uma confirmação negativa Sem rota @@ -497,12 +389,9 @@ Timeout RSSI Indicador de Força de Sinal Recebido, uma medida usada para determinar o nível de energia que está a ser recebido pela antena. Um valor mais elevado de RSSI geralmente indica uma conexão mais forte e mais estável. - servidor rsyslog Sats - Salvar Salvar - Guardar .CSV no armazenamento (apenas ESP32) Digitalizar Secundário @@ -511,16 +400,8 @@ Segurança Selecionar tudo Enviar - Enviar sino - Enviar sino com mensagem de alerta - Série - Taxa de transmissão série Configuração de Série - Consola de série - Série ativada - Modo de série - Servidor Definir a sua região definições @@ -537,21 +418,14 @@ Ecrã Slot - Posição Inteligente SNR Relação sinal-para-ruído, uma medida utilizada nas comunicações para quantificar o nível de um sinal desejado com o nível de ruído de fundo. Em Meshtastic e outros sistemas sem fio. Quanto mais alta for a relação sinal-ruído, menor é o efeito do ruído de fundo sobre a deteção ou medição do sinal. - SSID - Transmissão de estado (segundos) - Subnet Suportado Excluir Silenciar Tirar mute Servidor - Azul - Verde - Vermelho Telemetria Configuração de Telemetria Tema @@ -560,7 +434,6 @@ Padrão do sistema Timeout Data e hora - Ativar TLS Traçar rota Saltos em direção a %1$d Saltos de regresso %2$d @@ -572,7 +445,6 @@ Traçar rota - Enviar por LoRa LoRa MQTT @@ -589,21 +461,13 @@ Não monitorizado ou infraestrutura Tirar mute Desconhecido - Entrada Cima/Baixo/Selecionar ativa - Intervalo de atualização (segundos) Tempo ativo URL - - Usar formato de relógio 12h - Usar I2S como buzzer - Usar o modo INPUT_PULLUP - Usar um buzzer PWM Utilizador Configuração do Utilizador ID do utilizador - Utilizador via MQTT Voltagem Apagar o ponto de referência? diff --git a/core/resources/src/commonMain/composeResources/values-ro/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ro/schema_strings.xml new file mode 100644 index 0000000000..d5471805b4 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ro/schema_strings.xml @@ -0,0 +1,264 @@ + + + + + Albastru + Actual + Verde + Stare LED + Roșu + Prestabilit + Rată de eșantionare CODEC2 + CODEC 2 activat + Ieșire date I2S + Ceas I2S + Intrare date I2S + Selectare cuvânt I2S + Pin PTT + Bluetooth activat + Mod împerechere + Generează eveniment de intrare pe CCW + Generează eveniment de intrare pe CW + Generează eveniment de intrare la apăsare + Pin GPIO pentru portul encoder rotativ A + Pin GPIO pentru portul encoder rotativ B + Pin GPIO pentru portul encoder rotativ Press + Înapoi + Renunta + Niciunul + Selectați + Encoder rotativ #1 activat + Trimite clopoțel + Intrare sus/jos/selectare activată + Tip declanșator detectare + Senzor detectare activat + Pin GPIO de monitorizat + Nume comun + Trimite clopoțelul cu mesaj de alertă + Folosește modul INPUT_PULLUP + GPIO buton + GPIO buzzer + Apăsare dublă ca buton + Tratează o apăsare dublă pe accelerometrele compatibile ca apăsare a butonului utilizatorului. + Puls LED + Intervalul de difuzare a informațiilor nodului + Mod de redifuzare + Toate + Toate, emite decodarea + Același comportament ca „Toate”, dar omite decodarea pachetelor și le retransmite direct. Disponibil numai în rolul Releu. Setarea acestei opțiuni pentru orice alt rol va avea ca rezultat comportamentul „Toate” + Doar numere de port standard + Numai cunoscute + Numai local + Ignoră mesajele observate provenite de la rețele străine deschise sau pe care nu le poate decripta. Retransmite mesajele numai pe canalele locale primare/secundare ale nodurilor. + Niciunul + Permis numai pentru rolurile SENSOR, TRACKER și TAK_TRACKER, aceasta va inhiba toate retransmisiile, similar rolului CLIENT_MUTE. + Rolul dispozitivului + Client + Client bază + Client ascuns + Dispozitiv care transmite numai atunci când este necesar pentru a asigura discreția sau economisirea energiei. + Client mut + Dispozitiv care nu redirecționează pachetele de la alte dispozitive. + Pierdut și găsit + Transmite locația ca mesaj către canalul implicit în mod regulat pentru a ajuta la recuperarea dispozitivului. + Releu + Ruter + Ruter client + Ruter cu întârziere + Senzor + Transmite pachete telemetrice ca prioritate. + TAK + Optimizat pentru comunicarea de sistem ATAK, reduce emisiunile de rutină. + Tracker TAK + Activează transmisiile TAK PLI automate și reduce transmisiile de rutină. + Tracker + Transmite de poziție GPS ca prioritate. + Fus orar + Intervalul caruselului + Comută automat la pagina următoare de pe ecran ca un carusel, bazat pe intervalul specificat. + Indică mereu spre nord + Busola de pe ecran, în afara cercului, va indica întotdeauna nordul. + Orientarea busolei + Mod ecran + Suprascrie aspectul implicit al ecranului. + Rotire ecran + Rotire ecran vertical. + Direcție îngroșat + Îngroşează textul din antet de pe ecran. + Tip OLED + Suprascrie ecranul OLED automat. + Ecran pornit pentru + Cât timp rămâne ecranul pornit după ce butonul utilizatorului este apăsat sau mesajele sunt primite. + Unități de măsură afișate + Unitățile afișate pe ecranul dispozitivului. + Utilizaţi formatul ceasului 12h + Când este activat, dispozitivul va afișa pe ecran ora în format 12 ore. + Trezire la apăsare sau mișcare + Necesită ca dispozitivul dvs. să aibă un accelerometru. + LED de ieșire activ ridicat + LED alertă clopoțel + Buzzer alertă clopoțel + Vibrație alertă clopoțel + LED alertă mesaj + Buzzer alertă mesaj + Vibrație alertă mesaj + Notificare externă activată + LED ieșire (GPIO) + Buzzer ieșire (GPIO) + Ieșire vibrație (GPIO) + Utilizează I2S ca buzzer + Utilizează buzzer PWM + Lățime bandă + Slot pentru frecvenţă + Frecvența de funcționare a nodului dvs. este calculată pe baza regiunii, presetării modemului și a acestui câmp. Când valoarea este 0, slotul este calculat automat pe baza numelui canalului principal și se va modifica față de slotul public implicit. Reveniți la slotul public implicit dacă sunt configurate canale private principale și canale publice secundare. + Rata de codificare + Acceptă MQTT + Numărul de Hops + Setează numărul maxim de salturi, valoarea implicită fiind 3. Creșterea numărului de salturi crește și congestia și trebuie utilizată cu precauție. Mesajele de difuzare cu 0 salturi nu vor primi confirmări (ACK). + Ignoră MQTT + Presetări + Rază lungă - rapid + Rază lungă - moderat + Rază lungă - lent + Rază lungă - turbo + Rază medie - rapid + Rază medie - lent + Rază scurtă - rapid + Rază scurtă - lent + Rază scurtă - turbo + Rază foarte lungă - lent + Suprascrie ciclul de obligații + Suprascriere frecvență + Ventilator PA dezactivat + Regiune + Regiunea în care veți folosi radioul. + Factor de răspândire + Amplificare RX amplificată + Transmisie activată + Putere transmisie + Utilizare presetare + Retrimite observatorul + Medic + Lunetist + Lider de echipă + Membrii Echipei + Mesaj + Adresă + MQTT activat + Criptare activată + Parolă + Proxy-ul pentru client activat + Temă rădăcină + Nume de utilizator + Info vecin activat + Transmite peste LoRA + NeighborInfo al dvs. să fie transmis prin LoRa, pe lângă MQTT și PhoneAPI. Nu este disponibil pe un canal cu cheie și nume implicite. + Interval de actualizare GPS + Mod IPv4 + Activați transmisiunea pachetelor prin UDP în rețeaua locală. + Ethernet activat + Activarea Ethernet va dezactiva conexiunea Bluetooth la aplicație. Conexiunile TCP ale nodului nu sunt disponibile pe dispozitivele Apple. + DNS + Poartă de acces + IP + Server NTP + Niciunul + Difuzare UDP + server rsyslog + Activarea WiFi va dezactiva conexiunea Bluetooth la aplicație. + Parolă + Numele rețelei + Paxcounter activat + Interval de actualizare GPS + Distanță inteligentă + Modificarea minimă a distanței în metri care trebuie luată în considerare pentru o transmisie inteligentă a poziției. + Interval inteligent + Poziție fixă + GPIO EN GPS + Mod GPS (hardware fizic) + Interval de actualizare GPS + Cât de des ar trebui să încercăm să obținem o poziție GPS (<10sec păstrează GPS activat). + Activat + Interval de difuzare + Intervalul maxim care poate trece fără ca un nod să transmită o poziție. + Poziție inteligentă + Steaguri poziție + Câmpuri opționale care trebuie incluse la asamblarea mesajelor de poziție. Cu cât sunt incluse mai multe câmpuri, cu atât mesajul va fi mai mare, ceea ce va duce la un timp de transmisie mai lung și la un risc mai mare de pierdere a pachetelor. + Altitudine + Data si ora + GPIO recepție GPS + GPIO transmitere GPS + Raportul suprascrierii multiplicatorului ADC + Activează modul de economisire a energiei + Va păstra totul în repaus cât mai mult posibil, pentru rolul de tracker și senzor, aceasta va include și radioul LoRa. Nu utilizați această setare dacă doriți să utilizați dispozitivul cu aplicațiile telefonului sau dacă utilizați un dispozitiv fără buton de utilizator. + Închidere la pierderea de energie + Așteptați pentru durata Bluetooth + Testul de gamă activat + Salvați .CSV doar în memorie (ESP32) + Cheie Administrator + Cheia publică autorizată să trimită mesaje de administrare către acest nod. + Debug log API activat + Generarea de jurnale de depanare în timp real prin serial, vizualizarea și exportarea jurnalelor dispozitivelor cu poziția redactată prin Bluetooth. + Mod Gestionat + Dispozitivul este gestionat de un administrator de rețea, utilizatorul neputând accesa niciuna dintre setările dispozitivului. + Cheia privată + Chei publice + Consolă serială + Consolă serială prin API-ul Stream. + Rata baud-ului serial + Rata baud-ului serial + Echo activat + Serial activat + Mod serial + RX + Prestabilit + Prestabilit + Expirat + TX + Puls + istoric număr maxim de retur + Fereastra de returnare a istoricului + Server + Numarul de inregistrari + Albastru + Maro + Azuriu + Albastru închis + Verde închis + Verde + Mov + Maro + Portocaliu + Violet + Roșu + Albastru-verzui + Alb + Galben + Interval actualizare măsurători de calitate a aerului + Trimite telemetrie dispozitiv + Activează/dezactivează modulul de telemetrie al dispozitivului pentru a trimite metrici către rețeaua mesh. Acestea sunt valori nominale. Rețelele mesh congestionate se vor scala automat la intervale mai lungi, în funcție de numărul de noduri online. + Intervalul de actualizare a parametrilor dispozitivului + Valorile de mediu utilizează Fahrenheit + Modul de măsurare mediu activat + Valorile de mediu pe ecran sunt activate + Interval actualizare valori mediu + Modul de măsurare putere activat + Valori pe ecran activate + Interval actualizare măsurători de putere + Prag de pachet necunoscut + diff --git a/core/resources/src/commonMain/composeResources/values-ro/strings.xml b/core/resources/src/commonMain/composeResources/values-ro/strings.xml index e4bb88477f..eb8871ee37 100644 --- a/core/resources/src/commonMain/composeResources/values-ro/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ro/strings.xml @@ -26,7 +26,6 @@ Ștergeți mesajul Acţiuni Suprascriere multiplicator ADC - Raportul suprascrierii multiplicatorului ADC Adaugă Adaugă o notiță privată @@ -37,7 +36,6 @@ Adaugă Adaugă nivel rețea Adresă - Cheie Administrator Chei Admin Administrare Avansate @@ -45,23 +43,14 @@ Avansate Pictograma calităţii aerului - Interval actualizare măsurători de calitate a aerului Procentul de timp de emisie utilizat în ultima oră. AirUtil - - Buzzer alertă clopoțel - LED alertă clopoțel Caracter clopoțel de alertă! - Vibrație alertă clopoțel - Buzzer alertă mesaj - LED alertă mesaj - Vibrație alertă mesaj Toate Permite sursa de intrare Permite acces Pin nedefinit Alt Altitudine - Indică mereu spre nord Lumină ambientală Configurare iluminare ambientală Analytics sunt colectate pentru a ne ajuta să îmbunătățim aplicația Android (mulțumesc), vom primi informații anonime despre comportamentul utilizatorului. Aceasta include rapoarte de accidente, ecrane folosite în aplicație, etc. @@ -83,26 +72,18 @@ Pin-uri disponibile Înapoi Slab - - Lățime bandă Baro Baterie Adresa baterie INA_2XX I2C Dispozitive Bluetooth - Albastru Bluetooth Dispozitive bluetooth disponibile Configurare Bluetooth - Bluetooth activat Gestionați fără fir setările și canalele dispozitivului dvs. Descoperiți Bluetooth - Direcție îngroșat Setări - Interval de difuzare - GPIO buton - GPIO buzzer Calculare… Permisiune cameră Renunta @@ -112,7 +93,6 @@ Mesaj prestabilit activat Nu s-a putut schimba canalul, deoarece radioul nu este conectat încă. Vă rugăm să încercați din nou. Oprirea nu este acceptată pe acest dispozitiv - Intervalul caruselului Utilizarea pentru canalul curent, inclusiv TX bine format, RX și RX malformat (zgomot). Ch @@ -126,7 +106,6 @@ Canalul 8 Funcții canal Acest URL de canal este invalid și nu poate fi folosit - Canal Numele canalului Adresa canalului ChUtil @@ -147,9 +126,6 @@ Notificare client Închide Închideți selecția - CODEC 2 activat - Rată de eșantionare CODEC2 - Rata de codificare Restrânge graficul Comunicați în afara grilei cu prietenii și comunitatea dvs. fără serviciu celular. @@ -160,45 +136,20 @@ Este necesară permisiunea de localizare pentru a afișa distanța și rularea. Acest dispozitiv nu are un senzor de busolă. Heading este indisponibil. Vârf nord busolă - Orientarea busolei Busolă Suprafață estimată: \u00b1%1$s (\u00b1%2$s) Zonă estimată: precizie necunoscută Chei promise detectate, selectaţi OK pentru regenerare. - Tratează o apăsare dublă pe accelerometrele compatibile ca apăsare a butonului utilizatorului. Controlează LED-ul intermitent de pe dispozitiv. Pentru majoritatea dispozitivelor, acesta va controla unul dintre cele 4 LED-uri, LED-urile GPS și ale încrăcătorului nu pot fi controlate. - NeighborInfo al dvs. să fie transmis prin LoRa, pe lângă MQTT și PhoneAPI. Nu este disponibil pe un canal cu cheie și nume implicite. Trimite o poziție pe canalul principal când butonul utilizatorului este apăsat de trei ori. Fus orar pentru datele de pe ecranul dispozitivului și jurnal. Utilizați fusul orar al telefonului - Comută automat la pagina următoare de pe ecran ca un carusel, bazat pe intervalul specificat. - Busola de pe ecran, în afara cercului, va indica întotdeauna nordul. - Suprascrie aspectul implicit al ecranului. - Rotire ecran vertical. - Îngroşează textul din antet de pe ecran. - Suprascrie ecranul OLED automat. - Cât timp rămâne ecranul pornit după ce butonul utilizatorului este apăsat sau mesajele sunt primite. - Unitățile afișate pe ecranul dispozitivului. - Necesită ca dispozitivul dvs. să aibă un accelerometru. - Frecvența de funcționare a nodului dvs. este calculată pe baza regiunii, presetării modemului și a acestui câmp. Când valoarea este 0, slotul este calculat automat pe baza numelui canalului principal și se va modifica față de slotul public implicit. Reveniți la slotul public implicit dacă sunt configurate canale private principale și canale publice secundare. - Setează numărul maxim de salturi, valoarea implicită fiind 3. Creșterea numărului de salturi crește și congestia și trebuie utilizată cu precauție. Mesajele de difuzare cu 0 salturi nu vor primi confirmări (ACK). Presetările modemului disponibile, implicit este Long Fast (Rază lungă - rapid). Regiunea în care veți folosi radioul. - Activarea Ethernet va dezactiva conexiunea Bluetooth la aplicație. Conexiunile TCP ale nodului nu sunt disponibile pe dispozitivele Apple. Activați transmisiunea pachetelor prin UDP în rețeaua locală. - Intervalul maxim care poate trece fără ca un nod să transmită o poziție. - Modificarea minimă a distanței în metri care trebuie luată în considerare pentru o transmisie inteligentă a poziției. - Cea mai rapidă actualizare a poziției care va fi trimisă dacă distanța minimă a fost respectată. - Câmpuri opționale care trebuie incluse la asamblarea mesajelor de poziție. Cu cât sunt incluse mai multe câmpuri, cu atât mesajul va fi mai mare, ceea ce va duce la un timp de transmisie mai lung și la un risc mai mare de pierdere a pachetelor. - Cât de des ar trebui să încercăm să obținem o poziție GPS (<10sec păstrează GPS activat). - Va păstra totul în repaus cât mai mult posibil, pentru rolul de tracker și senzor, aceasta va include și radioul LoRa. Nu utilizați această setare dacă doriți să utilizați dispozitivul cu aplicațiile telefonului sau dacă utilizați un dispozitiv fără buton de utilizator. - Cheia publică autorizată să trimită mesaje de administrare către acest nod. - Generarea de jurnale de depanare în timp real prin serial, vizualizarea și exportarea jurnalelor dispozitivelor cu poziția redactată prin Bluetooth. - Dispozitivul este gestionat de un administrator de rețea, utilizatorul neputând accesa niciuna dintre setările dispozitivului. Utilizat pentru a crea o cheie partajată cu un dispozitiv la distanță. Generată din cheia publică și trimisă către alte noduri din rețea pentru a le permite să calculeze o cheie secretă comună. - Consolă serială prin API-ul Stream. Configuraţi permisiunile Bluetooth Configurează alertele critice Configurare permisiuni locație @@ -238,7 +189,6 @@ Filtru inclus Presetări filtre Filtre - Debug log API activat Reimprospatare Export jurnale Panou de depanare @@ -270,8 +220,6 @@ Detalii Senzor de detecție Configurare senzor detectare - Senzor detectare activat - Tip declanșator detectare Dispozitiv Configurare dispozitiv @@ -282,12 +230,9 @@ Valori dispozitiv %1$s %1$s:%2$s%% - Intervalul de actualizare a parametrilor dispozitivului %1$s: %2$s V Adormirea dispozitivului Dispozitiv de stocare & UI (doar cu permisiune) - Trimite telemetrie dispozitiv - Activează/dezactivează modulul de telemetrie al dispozitivului pentru a trimite metrici către rețeaua mesh. Acestea sunt valori nominale. Rețelele mesh congestionate se vor scala automat la intervale mai lungi, în funcție de numărul de noduri online. Tema %1$s, Limba %2$s Punct de rouă Mesaj direct @@ -302,53 +247,38 @@ Mesaje Selectat Disc liber %1$d - Ecran Ecran dispozitiv - Mod ecran - Când este activat, dispozitivul va afișa pe ecran ora în format 12 ore. - Unități de măsură afișate Distanță Filtre distanță Filtrează lista de noduri și harta plasei în funcție de proximitatea telefonului tău. Măsurătorile distanței Afișează distanța dintre telefonul dvs. și alte noduri Meshtastice cu poziții. - DNS Ștergeți căutarea MQTT Noduri Gata - Apăsare dublă ca buton Mesajele provenite de la o un gateway public de internet sunt redirecționate către rețeaua locală. Datorită politicii de zero salturi, traficul provenit de la serverul MQTT implicit nu se va propaga mai departe de acest dispozitiv. Descărcare Duplicat Cheie Publică detectată + Dinamic Înființarea cu ușurință a rețelelor private de plasare pentru comunicații sigure și fiabile în zonele îndepărtate; - Echo activat Editare Modifică sursa rețelei 8 Ore - Activează modul de economisire a energiei Activat - - Criptare activată Nepotrivire cheie publică Cheia publică nu corespunde cu cheia înregistrată. Puteți elimina nodul și permiteți schimbul de chei din nou, dar acest lucru poate indica o problemă de securitate mai gravă. Contactați utilizatorul printr-un alt canal de încredere, pentru a determina dacă schimbarea cheii s-a datorat unei resetări la setările din fabrică sau unei alte acțiuni intenționate. Criptare cu cheie publică Indicatori de mediu - Mediu - Modul de măsurare mediu activat - Valorile de mediu pe ecran sunt activate - Interval actualizare valori mediu - Valorile de mediu utilizează Fahrenheit Eroare Limita Duty Cycle a fost atinsă. Nu se pot trimite mesaje acum, vă rugăm să încercați din nou mai târziu. Opţiuni Ethernet - Ethernet activat IP Ethernet: Schimb de poziție Extindeți graficul @@ -358,7 +288,6 @@ Exportă toate pachetele Notificare externă Configurare notificare externă - Notificare externă activată Resetare la setările din fabrică Acceptabil Favorit @@ -368,10 +297,6 @@ Fișiere disponibile (%1$d): Adaugă cuvânt sau regex:pattern-ul - Dezactivați filtrarea - Activați filtrarea - Activați filtrarea - Ascunde mesajele ce conțin cuvinte filtre Ascunde %1$d filtrate Filtru Filtrat @@ -431,6 +356,7 @@ Acest lucru ar putea dura un minut... Target: %1$s Actualizare firmware + %1$d% Eroare necunoscuta Model hardware necunoscut: %1$d Lansare la distanţă necunoscută @@ -445,45 +371,23 @@ Se așteaptă ca dispozitivul să se repornească în modul OTA... Se așteaptă ca dispozitivul să se reconecte... Versiune firmware: %1$s - PIN fix Poziție fixă - Rotire ecran Pentru mai multe informații, consultați politica noastră de confidențialitate. Memorie Liberă Frecvență - Slot pentru frecvenţă - Nume comun Rezistența la gaz - Poartă de acces - Generează eveniment de intrare pe CCW - Generează eveniment de intrare pe CW - Generează eveniment de intrare la apăsare Generați codul QR Să începem Bun - GPIO Pin GPIO - Pin GPIO pentru portul encoder rotativ A - Pin GPIO pentru portul encoder rotativ B - Pin GPIO pentru portul encoder rotativ Press - Pin GPIO de monitorizat - GPIO EN GPS - Mod GPS (hardware fizic) - GPIO recepție GPS - GPIO transmitere GPS - Verde Dispozitive Model hardware Direcție - Puls Ascunde Layer Ascundeți parola - istoric număr maxim de retur - Fereastra de returnare a istoricului - Numărul de Hops Salturi distanță Gazdă Valori Gazdă @@ -491,18 +395,12 @@ Sunt de acord Am citit şi înţeleg cele de mai sus. Sunt de acord voluntar cu transmiterea necriptată a datelor nodului prin MQTT Știu ce fac. - Ceas I2S - Intrare date I2S - Ieșire date I2S - Selectare cuvânt I2S IAQ (Calitatea aerului interior) valoarea IAQ pe o scară relativă, măsurată cu Bosch BME680. Intervalul valorilor: 0–500. Semne pictograme - Ignoră Adaugă '%1$s' in lista de ignor? Radioul tău va reporni după ce această modificare. Ignoră primirea - Ignoră MQTT Elimină '%1$s' din lista de ignor? Radioul tău va reporni după această modificare. Importă configurația @@ -521,7 +419,6 @@ IP Adresa IP: Port: - Mod IPv4 Ieșire JSON activată %1$s @@ -534,8 +431,6 @@ Ultimul stabil Latitudine - Puls LED - Stare LED Canal implicit de administrator Librării %1$d @@ -596,11 +491,9 @@ Lux Gestionează surse personalizate de stil Gestionează Layers Hartă - Mod Gestionat Solicitarea de poziție manuală este necesară Harta retea - Capacitate cache: %1$d MB\nUtilizare cache: %2$d MB Manager cache Dimensiunea actuală a cache-ului %1$d secțiuni @@ -615,11 +508,8 @@ Manager offline Ștergerea cache-ului SQL a eșuat, vedeți logcat pentru detalii Cache SQL șters pentru %1$s - Raportarea hărții Consimțământ pentru a Partaja date Node necriptate prin MQTT Prin activarea acestei caracteristici, acceptați și consimți în mod expres transmiterea locației geografice în timp real a dispozitivului dvs. peste protocolul MQTT fără criptare. Aceste date de localizare pot fi utilizate în scopuri cum ar fi raportarea hărților live, urmărirea dispozitivelor și funcțiile telemetrice aferente. - Intervalul de raportare hartă (secunde) - Nodul tău va trimite periodic un pachet de rapoarte de hărți necriptate serverului MQTT configurat, acesta include un nume id, lung și scurt, aproximează locația, modelul hardware, rolul, versiunea firmware, regiunea LoRa, presetarea modemului și numele canalului primar. Selectați regiunea pentru descărcare Pornește descărcarea Selecție stil hartă @@ -664,9 +554,6 @@ Rutare prin lanțul SF++… Necunoscut Mesaje - Difuzare minimă (secunde) - Distanță inteligentă - Interval inteligent Durata minimă a trezirii Presetări Configurare modul @@ -676,7 +563,6 @@ MQTT Configurare MQTT - MQTT activat Conectat Deconectat Trebuie să alegeți o regiune! @@ -692,14 +578,12 @@ Silențios pentru %1$d zile, %2$s ore Silențios pentru %1$s ore Nu este silențios - Durată notificare (secunde) Nume Numele nu poate fi gol Navigați înapoi Navigați în Informații vecin Configurare informații vecin - Info vecin activat Rețea Am primit un nou URL de canal Mesaje noi mai jos @@ -743,7 +627,6 @@ după favorite via MQTT Resetare NodeDB - Intervalul de difuzare a informațiilor nodului Noduri Noduri în această locație @@ -755,6 +638,7 @@ Neconectat Notiță Notițe + Meshtastic folosește notificări pentru a te ține la curent cu mesaje noi și alte evenimente importante. Puteți actualiza permisiunile de notificare în orice moment din setări. Notificări pentru canal și mesaje directe. @@ -763,12 +647,8 @@ Notificări la primirea alertei/clopoțelului Notificări la primirea mesajului Acum - Server NTP - Numarul de inregistrari - Acceptă MQTT Ok - Tip OLED 24 Ore 1 Oră @@ -781,19 +661,9 @@ Biblioteci open source Opțiuni Orientare spre nord - - Buzzer ieșire (GPIO) - Durată ieșire (milisecunde) - LED de ieșire activ ridicat - LED ieșire (GPIO) - Ieșire vibrație (GPIO) Meniu de Overflow Suprascrie portul serial al consolei - Suprascrie ciclul de obligații - Suprascriere frecvență - Ventilator PA dezactivat - Mod împerechere Parolă PAX @@ -805,7 +675,6 @@ W:%1$d Paxcounter Configurație Paxcounter - Paxcounter activat Pozitie periodica Meshtastic necesită permisiunea „Dispozitive din apropiere” pentru a găsi și conecta dispozitive prin Bluetooth. Puteți dezactiva această funcție când nu o utilizați. @@ -818,20 +687,16 @@ %1$d ore %1$d de ore + Poziție Setează din locația curentă a telefonului Poziție activată - Steaguri poziție Poziție Pachet de poziție - Alimentare Configurare Putere Valori putere - Modul de măsurare putere activat - Valori pe ecran activate - Interval actualizare măsurători de putere Alimentare Locație precisă Limba @@ -852,12 +717,9 @@ Text Primară Poziție periodică și transmisiune telemetrică - Cheia privată Furnizați locația telefonului la mesh Nume furnizor exista. - Proxy-ul pentru client activat PSK - Pin PTT Chei publice Cheie publică schimbată Cod QR @@ -875,23 +737,12 @@ Ploaie (24h) Test de rază Configurare test interval - Testul de gamă activat Reacţionează Restartează - - Mod de redifuzare - Retransmite orice mesaj observat, dacă acesta se afla pe canalul nostru privat sau provine de la o altă rețea cu aceiași parametri LoRa. - Același comportament ca „Toate”, dar omite decodarea pachetelor și le retransmite direct. Disponibil numai în rolul Releu. Setarea acestei opțiuni pentru orice alt rol va avea ca rezultat comportamentul „Toate” - Ignoră pachetele provenite de la numere de port non-standard, cum ar fi: TAK, RangeTest, PaxCounter etc. Retransmite numai pachetele cu numere de port standard: NodeInfo, Text, Position, Telemetry și Routing. - Ignoră mesajele observate din rețele străine, cum ar fi „Numai local”, dar merge mai departe, ignorând și mesajele de la noduri care nu se află deja în lista cunoscută a nodului. - Ignoră mesajele observate provenite de la rețele străine deschise sau pe care nu le poate decripta. Retransmite mesajele numai pe canalele locale primare/secundare ale nodurilor. - Permis numai pentru rolurile SENSOR, TRACKER și TAK_TRACKER, aceasta va inhiba toate retransmisiile, similar rolului CLIENT_MUTE. Dispozitive recente de rețea - Roșu Reimprospatare Sunteţi sigur că doriţi să vă regeneraţi cheia privată?\n\nNodurile care ar fi putut schimba anterior chei cu acest modul vor trebui să elimine acel nod și să schimbe din nou tastele pentru a relua comunicarea securizată. Regenerează Cheia privată - Regiune De la distanta Administrare la distanță @@ -930,31 +781,17 @@ Rolul dispozitivului Client Client bază - Tratează pachetele provenite de la sau destinate nodurilor favorite ca ROUTER_LATE, iar toate celelalte pachete ca CLIENT. - Dispozitiv de mesagerie conectat la aplicație sau independent. Client ascuns - Dispozitiv care transmite numai atunci când este necesar pentru a asigura discreția sau economisirea energiei. Client mut - Dispozitiv care nu redirecționează pachetele de la alte dispozitive. Pierdut și găsit Releu - Nod de infrastructură pentru extinderea acoperirii rețelei prin retransmiterea mesajelor cu un consum suplimentar minim. Nu este vizibil în lista de noduri. Ruter Ruter client - Combinație între ROUTER și CLIENT. Nu este compatibil cu dispozitivele mobile. - Nod de infrastructură pentru extinderea acoperirii rețelei prin retransmiterea mesajelor. Vizibil în lista de noduri. Ruter cu întârziere - Nod de infrastructură care retransmite întotdeauna pachetele o singură dată, dar numai după toate celelalte moduri, asigurând acoperire suplimentară pentru clusterele locale. Vizibil în lista de noduri. Senzor - Transmite pachete telemetrice ca prioritate. TAK - Optimizat pentru comunicarea de sistem ATAK, reduce emisiunile de rutină. Tracker TAK - Activează transmisiile TAK PLI automate și reduce transmisiile de rutină. Tracker - Transmite de poziție GPS ca prioritate. - Temă rădăcină - Encoder rotativ #1 activat S-a primit o confirmare negativă Nici o rută @@ -962,12 +799,9 @@ Expirat RSSI Indicatorul intensității semnalului recepționat (Received Signal Strength Indicator), o măsurătoare utilizată pentru a determina nivelul de putere recepționat de antenă. O valoare RSSI mai mare indică, în general, o conexiune mai puternică și mai stabilă. - server rsyslog Sateliți - Salvează Salvează - Salvați .CSV doar în memorie (ESP32) Exportă pachetele rangetest Scanare @@ -977,7 +811,6 @@ Scanare cod QR contacte partajat Scanare… Scanare… - Ecran pornit pentru Derulare până jos Căutare emoji-uri... Secundar @@ -1005,19 +838,8 @@ Selectat Tipul hărții selectate Trimite - Trimite clopoțel - Trimite clopoțelul cu mesaj de alertă - Interval mesaj expeditor (secunde) - Serial - Rata baud-ului serial Configurație serial - Consolă serială - Serial activat - Mod serial - RX - TX - Server Setează-ți regiunea setari @@ -1037,7 +859,6 @@ Arată repere Oprire Nod: %1$s - Închidere la pierderea de energie ⚠️ Aceasta va OPRI nodul. Pentru a-l repune în funcțiune, va fi necesară o intervenție fizică. Semnal Calitatea semnalului @@ -1045,16 +866,12 @@ Ecran Treci peste Slot - Poziție inteligentă SNR Raportul semnal-zgomot (Signal-to-Noise Ratio), o măsură utilizată în comunicații pentru a cuantifica nivelul unui semnal dorit în raport cu nivelul zgomotului de fond. În Meshtastic și în alte sisteme wireless, un SNR mai mare indică un semnal mai clar, care poate îmbunătăți fiabilitatea și calitatea transmiterii datelor. Umid sol Temp sol Viteza %1$d Km/h - Factor de răspândire - Numele rețelei - Difuzare stare (secunde) Mesaj de stare: Rămâneţi conectat oriunde Durată maximă de somn @@ -1062,39 +879,12 @@ Sprijinită de comunitatea Meshtastic Șterge Activare sunet - Amplificare RX amplificată Setări ale sistemului TAK (ATAK) Configurare TAK - Rolul membrului - Retrimite observatorul - Sediul Principal - caine - Medic - Operator Radio Telefon - Lunetist - Lider de echipă - Membrii Echipei - Nespecificat Activare server TAK local Server - Culoarea echipei - Albastru - Maro - Azuriu - Albastru închis - Verde închis - Verde - Mov - Maro - Portocaliu - Violet - Roșu - Albastru-verzui - Nespecificat - Alb - Galben Telemetrie Configurare telemetrie Temp @@ -1103,10 +893,8 @@ Luminos Setarea telefonului Timp - Fus orar Expirat Data si ora - TLS activat Comută poziția mea Trasare traseu @@ -1148,7 +936,6 @@ Păstrează Hops Router Prag de pachet necunoscut - Transmite peste LoRA LoRa MQTT @@ -1157,8 +944,6 @@ 24H 48 Ore 2W - Transmisie activată - Putere transmisie Tip Scrie un mesaj @@ -1175,9 +960,6 @@ Activare sunet Nerecunoscut Nesetat - 0 - Intrare sus/jos/selectare activată - Interval de actualizare GPS - Interval de actualizare (secunde) Actualizat Mesajele de la mesageria va fi trimise pe internet public prin intermediul oricărui portal configurat de nod. Timp de functionare @@ -1187,13 +969,7 @@ URL-ul trebuie să conţină substituenţi. Şablon URL USB - - Utilizaţi formatul ceasului 12h Codare compactă pentru chirilică - Utilizează I2S ca buzzer - Folosește modul INPUT_PULLUP - Utilizare presetare - Utilizează buzzer PWM Utilizator Configurare utilizator @@ -1201,13 +977,10 @@ Informații utilizator Șir Utilizator Info utilizator - Nume de utilizator LUX UV via MQTT Vizualizați pe hartă Tensiune - Așteptați pentru durata Bluetooth - Trezire la apăsare sau mișcare Avertizare Şterge waypointul? Editează waypoint diff --git a/core/resources/src/commonMain/composeResources/values-ru/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-ru/schema_strings.xml new file mode 100644 index 0000000000..7d5330297a --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-ru/schema_strings.xml @@ -0,0 +1,408 @@ + + + + + Синий + Уровень синего светодиода фоновой подсветки. + Ток + Ток управления для выхода светодиода. + Зеленый + Уровень зелёного канала светодиода подсветки. + Состояние LED + Состояние светодиода (вкл./выкл.) + Красный + Уровень красного канала светодиода подсветки. + 1200 бит/с + 1300 бит/с + 1400 бит/с + 1600 бит/с + 2400 бит/с + 3200 бит/с + 450 бит/с + 700C бит/с + По умолчанию + Частота дискретизации CODEC2 + Используемая скорость передачи Codec2. Частота дискретизации всегда 8 кГц. Более низкие скорости используют меньше полосы пропускания, но снижают качество звука. + CODEC 2 включен + Включить кодирование/декодирование звука Codec2 для голосовой связи по mesh-сети. + I2S Data Out + I2S Clock + I2S Data In + I2S Word Select + Контакт PTT + Номер GPIO-вывода для кнопки передачи (push-to-talk). + Bluetooth включен + Включить Bluetooth на устройстве + Фиксированный PIN-код + Фиксированный PIN-код для подключения к Bluetooth. Используется при соединении с фиксированным PIN-кодом + Режим сопряжения + Стратегия подключения Bluetooth + Фиксированный PIN-код + Без PIN-кода (просто работает) + Случайный PIN-код + Создать событие ввода для CCW + Входящее событие для вращения против часовой стрелки + Создать событие ввода для CW + Входящее событие для вращения по часовой стрелке + Создать событие ввода при нажатии + Входящее событие для нажатия энкодера + Контакт GPIO для порта A поворотного энкодера + GPIO-вывод для порта A поворотного энкодера. + Контакт GPIO для порта B поворотного энкодера + GPIO-вывод для порта B поворотного энкодера. + Контакт GPIO для порта Press поворотного энкодера + GPIO-вывод для кнопки поворотного энкодера. + Назад + Отмена + Вниз + Влево + Отсутствует + Вправо + Выбрать + Вверх + Поворотный энкодер #1 включен + Включить поворотный энкодер + Отправить колокольчик + Отправлять символ звонка вместе с сообщениями + Вверх/Вниз/Выбирать включён + Включить ввод вверх/вниз/выбор + Тип триггера обнаружения + Тип события-триггера + Датчик определения включен + Включает модуль датчика обнаружения; он должен быть включён как на ноде с датчиком, так и на всех нодах, которые должны получать текстовые сообщения датчика обнаружения или просматривать его журнал и график. + Минимальное время между передачами обнаружения + Минимальное время между передачами обнаружения. + GPIO контакт для мониторинга + GPIO-вывод, отслеживаемый на изменения состояния. + Понятное имя + Имя датчика для сообщений mesh-сети + Отправить колокол с уведомлением + Отправлять ASCII-звонок вместе с сообщением оповещения. Полезно для запуска внешнего уведомления по звонку. + Интервал передачи состояния + Как часто отправлять состояние датчика обнаружения в mesh-сеть, независимо от того, было что-то обнаружено или нет. + Любой фронт (высокий уровень) + Любой фронт (низкий уровень) + Нисходящий фронт + Высокий + Низкий + Восходящий фронт + Использовать режим INPUT_PULLUP + Использовать ли режим INPUT_PULLUP для GPIO-вывода. Применимо только если плата использует подтягивающие резисторы на этом выводе + Кнопка GPIO + GPIO-вывод для пользовательской кнопки; можно переназначить на платах с несколькими кнопками + Зуммер GPIO + GPIO-вывод для ШИМ-зуммера + Отключить тройное нажатие кнопки + Отключает горячую клавишу тройного нажатия пользовательской кнопки. + Двойное нажатие как кнопка + Рассматривать двойное нажатие на поддерживаемых акселерометрах как нажатие пользовательской кнопки. + Сердцебиение светодиодом + Управляет мигающим светодиодом на устройстве. На большинстве устройств это управляет одним из до 4 светодиодов; светодиоды зарядки и GPS не управляются. + Интервал вещания передачи информации об узле + Как часто отправляется информация о ноде. По умолчанию — 900 секунд. + Режим ретрансляции + Всё + Ретранслировать любое замеченное сообщение, если оно было в нашем приватном канале или в другом канале с теми же параметрами LoRa. + Все пропущенные декодирования + Так же, как и ALL, но пропускает декодирование пакетов и просто ретранслирует их. Доступно только в роли Repeater. Установка этого параметра для любых других ролей приведет к изменению поведения ALL. + Только основные номера портов + Ретранслирует только пакеты основных portnum: NodeInfo, Text, Position, Telemetry и Routing. + Только известные + Игнорирует замеченные сообщения из чужих mesh-сетей, как и режим "Только локально", но идёт дальше: также игнорирует сообщения от нод, которых нет в списке известных нод. + Только локальные + Игнорирует обнаруженные сообщения из чужих mesh-сетей, которые открыты или не могут быть расшифрованы. Ретранслирует сообщение только на локальных основных / дополнительных каналах нод. + Отсутствует + Разрешено только для ролей SENSOR, TRACKER и TAK_TRACKER, это запретит все ретрансляции, не похожие на роль CLIENT_MUTE. + Роль устройства + Client + Client Base + Используется для нод на крышах, чтобы шире распространять сообщения от нескольких nearby client mute нод. + Устройство для обмена сообщениями, подключённое к приложению или работающее автономно. + Client Hidden + Устройство, которое передает сигнал только при необходимости для скрытности или экономии энергии. + Client Mute + Устройство, которое не пересылает пакеты с других устройств. + Lost and Found + Регулярно передает местоположение в виде сообщения на канал по умолчанию для помощи в восстановлении устройства. + Repeater + Устаревшая инфраструктурная роль, создающая разрывы в цепочке ретрансляции mesh-сети. Переключите эту ноду на роль на основе Router (Router или Router Late). + Router + Router Client + Инфраструктурная нода только для вышки или вершины горы. Не использовать на крышах или в мобильных нодах. Требует исключительного покрытия. Отображается в списке нод. + Router Late + Инфраструктурная нода, которая всегда ретранслирует пакеты один раз, но только после всех остальных режимов. Отображается в списке нод. Не лучший выбор для нод на крышах. + Sensor + Транслирует пакеты телеметрии в приоритетном порядке. + Тактический + Оптимизировано для связи с системой ATAK, сокращает текущие передачи. + TAK Tracker + Включает автоматические трансляции TAK PLI и сокращает рутинные трансляции. + Tracker + Транслирует пакеты местоположения GPS в приоритетном порядке. + Часовой пояс + Строка определения часового пояса POSIX + Интервал карусели + Автоматически переключает на следующую страницу на экране как карусель, основываясь на заданном интервале. + Всегда указывать на север + Стрелка компаса на экране за пределами круга всегда указывает на север. + Направление компаса + Указывает, как повернуть или инвертировать вывод компаса для точного отображения. + 0° + 0° инвертировано + 180° + Режим экрана + Переопределить макет экрана по умолчанию. + Британская + Метрическая + Повернуть экран + Отразить экран по вертикали. + Выделять заголовок жирным + Жирный шрифт заголовка на экране. + Тип OLED + Переопределить автоматическое распознавание экрана OLED. + Включать экран на + Как долго экран остается включенным после нажатия пользовательской кнопки или получения сообщений. + Система измерения + Единицы измерения, отображаемые на экране устройства. + Использовать 12-часовой формат времени + Если включено, устройство будет отображать время на экране в 12-часовом формате. + Включать экран при касании или движении + Необходимо наличие акселерометра на вашем устройстве. + Вывод светодиода активный высокий + Светодиодный индикатор + Бузер оповещений + Вибросигнал + LED-индикатор уведомлений + Звуковой уведомитель сообщений + Вибрация при уведомлении + Внешние уведомления включены + Интервал повтора + Выход LED (GPIO) + Выход Буззера (GPIO) + Вибросигнал (GPIO) + Использовать I2S как буззер + Использовать PWM-звукоизлучатель + Ширина канала + Частота слота + Рабочая частота вашей ноды рассчитывается на основе региона, настроек модема и этого поля. При значении 0 интервал автоматически рассчитывается на основе названия основного канала и изменяется с публичного интервала по умолчанию. Вернитесь к публичному интервалу по умолчанию, если настроены частный основной и общедоступный дополнительный каналы. + Частота кодирования + ОК в MQTT + Количество прыжков + Задает максимальное количество прыжков, по умолчанию - 3. Увеличение количества также увеличивает перегрузку и должно использоваться с осторожностью. Сообщения с 0 прыжков не будут получать подтверждения. + Игнорировать MQTT + Шаблоны + Легкий - Быстро + Легкий - Медленно + Большая дальность - Быстрый + Большая дальность - Умеренно + Long Range - Slow + Большая дальность - Турбо + Medium Range - Fast + Medium Range - Slow + Medium Range - Turbo + Узкий - Быстро + Узкий - Медленно + Short Range - Fast + Short Range - Slow + Short Range - Turbo + Tiny - Fast + Tiny - Slow + Очень большая дальность - Медленный + Переопределить рабочий цикл + Переопределить частоту + PA вентилятор выключен + Регион / Страна + Регион, в котором вы будете использовать ваше радио. + Регион МСЭ 3 / Любительский 2 м + Регион МСЭ 3 / Любительский 70 см + Япония + Корея + Казахстан 433 МГц + Казахстан 863 МГц + 2,4 ГГц + Малайзия 433 МГц + Малайзия 919 МГц + Непал 865 МГц + Новая Зеландия 865 МГц + Филиппины 433 МГц + Филиппины 868 МГц + Филиппины 915 МГц + Россия + Сингапур 923 МГц + Таиланд + Тайвань + Украина 433 Мгц + Украина 868 МГц + Пожалуйста, выберите регион + Соединенные Штаты Америки + Коэффициент распространения + Количество чирпов на символ — 2 в степени этого значения. + Усиление RX + Включить режим усиленного усиления приёма (RX boosted gain) на радио на базе SX126X + Передача включена + Разрешить передачу по LoRa-радио. Отключай при горячей замене антенн или стендовых испытаниях. + Мощность передатчика + Мощность передачи радио. Оставь ноль, чтобы использовать максимально допустимый уровень для региона — именно так следует поступать большинству радиоустройств. + Использовать шаблон + Использовать настройки пресета модема вместо ручного указания полосы пропускания, коэффициента расширения и скорости кодирования + Интервал публикации на карте + Как часто публикуется отчёт для карты. + Сообщать местоположение + Я прочитал и понимаю вышесказанное. Я добровольно соглашаюсь на незашифрованную передачу данных моей ноды через MQTT. + Вперёдсмотрящий + HQ + K9 + Санитар + RTO + Снайпер + Руководитель команды + Участник команды + По умолчанию (Участник команды) + Интервал + Как часто передаётся маяк. + Сообщение + Сообщение для передач маяка + Адрес + Адрес MQTT-сервера + MQTT включен + Включить MQTT-шлюз + Шифрование включено + Отправлять зашифрованные пакеты в MQTT + Публикация на карте + Если включено, нода будет периодически отправлять незашифрованный отчёт на MQTT-сервер для отображения на онлайн-картах. Отчёт включает имя, ID, позицию, данные об оборудовании и т.д. + + Пароль + Пароль MQTT + Прокси клиенту включен + Использует сетевое подключение вашего телефона для подключения к MQTT. + Корневая тема + Включить TLS + Имя пользователя + Информация о соседях включена + Передать через LoRa + В дополнение к отправке на MQTT и PhoneAPI, наши NeighborInfo должны быть переданы через LoRa. Недоступно на канале с ключом и именем по умолчанию. + Интервал опроса GPS + Режим IPv4 + Включить вещание пакетов через UDP в локальной сети. + Ethernet включен + Включение Ethernet отключит Bluetooth-соединение с приложением. TCP-соединения не доступны на устройствах Apple. + Служба доменных имен (DNS) + Шлюз + IP-адрес + Подсеть + NTP-сервер + Отсутствует + Трансляция UDP + Сервер rsyslog + Wi-Fi включен + Включение WiFi отключит Bluetooth-подключение к приложению. + Пароль + Название сети + BLE RSSI порог (по умолчанию -80) + Paxcounter включен + Интервал опроса GPS + Порог WiFi RSSI (по умолчанию -80) + Умное расстояние + Минимальное расстояние для умной рассылки координаты. + Умный интервал + Фиксированная позиция + GPIO EN GPS + Режим GPS (физическое оборудование) + Интервал опроса GPS + Как часто запрашивать координаты GPS (<10 секунд держит GPS включенным постоянно). + Включено + Включено + Период рассылки + Максимальное время между передачами позиции нодой. + Умная позиция + Флаги позиции + Необязательные поля в сообщении о местоположении. Чем больше полей включено, тем больше сообщение, а значит больше время передачи и риск потери пакетов. + Высота + Высота относительно уровня моря + Геоидная поправка высоты + Направление движения + Количество спутников + Порядковый номер пакета + Скорость транспорта + Отметка времени + GPIO приёма GPS + GPIO передачи GPS + Коэффициент переопределения ADC + Включить режим энергосбережения + Все компоненты устройства будут засыпать, насколько возможно. Для ролей TRACKER или SENSOR также будет засыпать радиопередатчик. Не включай режим, если используешь ноду с телефоном или у ноды нет кнопки для пробуждения. + Выключение при потере мощности + Длительность ожидания Bluetooth + Проверка дальности включена + Сохранить .CSV в хранилище (только ESP32) + Ключ администратора + Открытый ключ для администрирования данной ноды + API журнала отладки включен + Вывод журнала отладки по последовательному каналу, просматривайте и выгружайте журналы устройства с измененным местоположением по Bluetooth. + Управляемый режим + Устройство управляется сетевым администратором, пользователь не может изменять настройки устройства. + Приватный ключ + Используется для создания открытого ключа с удаленным устройством + Публичный ключ + Консоль COM-порта + Последовательная консоль через Stream API. + Скорость COM-порта + Скорость COM-порта + Echo включен + COM-порт включен + Режим COM-порта + RX + По умолчанию + По умолчанию + Время ожидания истекло + TX + Store & Forward включена + Heartbeat + Макс возврат истории + Окно возврата истории + Сервер + Количество записей + Роль + Командой + Синий + Коричневый + Голубой + Тёмно-синий + Тёмно-зеленый + Зеленый + Пурпурный + Бордовый + Оранжевый + Фиолетовый + Красный + Бирюзовый + Белый + Жёлтый + Модуль измерения качества воздуха включен + Интервал обновления данных качества воздуха + Отправлять телеметрию устройства + Включите/выключите модуль телеметрии устройства, чтобы отправлять показатели в сеть. Это номинальные значения. Перегруженные сети будут автоматически масштабироваться на более длительные интервалы в зависимости от количества подключенных нод. + Интервал обновления метрик устройства + Использовать метрику окружения в Fahrenheit + Модуль метрик окружения включен + Показатели окружения на экране включены + Интервал обновления метрик среды + Модуль метрик питания включен + Включить метрики питания на экране + Интервал обновления метрик электропитания + Порог передач неизвестных пакетов + diff --git a/core/resources/src/commonMain/composeResources/values-ru/strings.xml b/core/resources/src/commonMain/composeResources/values-ru/strings.xml index 17c903f381..357a74ee42 100644 --- a/core/resources/src/commonMain/composeResources/values-ru/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ru/strings.xml @@ -17,18 +17,20 @@ --> + Влажность %1$s: %2$s От %1$s: %2$s заряд батареи %1$d + Канал %1$d %1$s отсюда избранный - %1$d hops away последний раз услышан %1$s оффлайн онлайн роль %1$s сигнал %1$s Хоп %1$d: %2$d нод + Температура О приложении Принять Благодарности @@ -46,7 +48,6 @@ Перевести сообщение Действия Коэффициент переопределения ADC - Коэффициент переопределения ADC Напряжение АЦП Добавить @@ -60,7 +61,6 @@ Добавить устройство вручную… Добавить сетевой уровень Адрес - Ключ администратора Ключи администратора Администрирование Расширенные @@ -70,26 +70,17 @@ Качество воздуха Значок качества воздуха Журнал метрик качества воздуха - Модуль измерения качества воздуха включен - Интервал обновления данных качества воздуха Процент времени эфира для передачи в течение последнего часа. AirUtil - - Бузер оповещений - Светодиодный индикатор Символ колокольчика оповещения! - Вибросигнал - Звуковой уведомитель сообщений - LED-индикатор уведомлений - Вибрация при уведомлении Всё Разрешить источник ввода Разрешить неопределённый контакт Уровень моря Высота - Всегда указывать на север Фоновое освещение Настройки Ambient Lighting + Аммоний Аналитика помогает нам улучшить Android приложение (спасибо), мы будем получать анонимизированную информацию о поведении пользователя. В частности: отчеты о сбоях, используемые экраны и пр. Разрешить аналитику и отчеты о сбоях. Платформы для аналитики: @@ -126,6 +117,7 @@ %1$d (%2$s) Очень вредный Архивный канал + Этот канал больше не настроен на твоём радио. Его сообщения сохранены здесь, но ты не можешь отправлять их или реагировать. Вы уверены? Вы уверены, что хотите перейти на канал по умолчанию? Звук @@ -136,8 +128,6 @@ Сохранить публичные и приватные ключи в безопасном зашифрованном хранилище на этом устройстве. Резервная копия и восстановление Плохой - - Ширина канала По умолчанию (%1$s кГц) %1$s кГц Неподдерживаемый (%1$s) @@ -145,16 +135,14 @@ Давл Батарея I2C-адрес INA_2XX батареи + БПК Устройства Bluetooth - BLE RSSI порог (по умолчанию -80) Для сканирования через Bluetooth на этой версии Android тоже нужно включить определение местоположения. Твоё местоположение не используется. - Синий Bluetooth Доступные Bluetooth-устройства Настройка Bluetooth Bluetooth выключен. Включи его, чтобы искать находящиеся рядом устройства. - Bluetooth включен Настройки Беспроводное управление настройками устройства и каналами. Обнаружение @@ -177,16 +165,12 @@ Достигнут предел сканирования Bluetooth. Попробуйте снова через %1$d секунд. Достигнут предел сканирования Bluetooth. Попробуйте снова через %1$d секунд. - Выделять заголовок жирным Сопряжение не удалось. Дай соседнему устройству разрешения и попробуй снова. Сопряжение не завершилось. Попробуй снова. Разрешение на доступ к ближайшим устройствам выключено, поэтому к твоему радио нельзя подключиться через Bluetooth. Нажми, чтобы включить его снова. Meshtastic не может переподключиться Настройки - Период рассылки Busy floor - Кнопка GPIO - Зуммер GPIO Снижение этого параметра навсегда удалит сохранённую историю для %1$d устройства. Снижение этого параметра навсегда удалит сохранённую историю для %1$d устройств. @@ -206,7 +190,6 @@ Шаблонные сообщения включены Не удалось сменить канал, поскольку радиостанция еще не подключена. Пожалуйста, попробуйте еще раз. Выключение не поддерживается на этом устройстве - Интервал карусели Использование для текущего канала, включая хорошо сформированный TX, RX и неправильно сформированный RX (так называемый шум). Кнл @@ -220,11 +203,11 @@ Канал 8 Особенности канала Этот URL-адрес канала недействителен и не может быть использован - Канал Имя канала URL канала ChUtil Каналы + ХПК Щебетун Весёлый помощник @@ -265,9 +248,6 @@ CO₂ Влажн CO₂ Темп CO₂ - CODEC 2 включен - Частота дискретизации CODEC2 - Частота кодирования Свернуть диаграмму Развален Общайтесь вне сети со своими друзьями и сообществом без использования сотовой связи. @@ -280,49 +260,28 @@ Для отображения расстояния и азимута требуется разрешение на определение местоположения. Это устройство не оснащено датчиком компаса. Курс недоступен. Север компаса в верх - Направление компаса Компас Предполагаемая площадь: \u00b1%1$s (\u00b1%2$s) Предполагаемая площадь: точность неизвестна Обнаружены скомпрометированные ключи, нажмите OK для пересоздания. - Рассматривать двойное нажатие на поддерживаемых акселерометрах как нажатие пользовательской кнопки. Управляет мигающим светодиодом на устройстве. Для большинства устройств это будет управлять одним из до 4 светодиодов, зарядное устройство и GPS светодиоды не управляются. - В дополнение к отправке на MQTT и PhoneAPI, наши NeighborInfo должны быть переданы через LoRa. Недоступно на канале с ключом и именем по умолчанию. Отправлять позицию на основной канал по тройному нажатию кнопки. Часовой пояс дат на экране и журнале устройства. Использовать часовой пояс телефона - Автоматически переключает на следующую страницу на экране как карусель, основываясь на заданном интервале. - Стрелка компаса на экране за пределами круга всегда указывает на север. - Переопределить макет экрана по умолчанию. - Отразить экран по вертикали. - Жирный шрифт заголовка на экране. - Переопределить автоматическое распознавание экрана OLED. - Как долго экран остается включенным после нажатия пользовательской кнопки или получения сообщений. - Единицы измерения, отображаемые на экране устройства. - Необходимо наличие акселерометра на вашем устройстве. - Рабочая частота вашей ноды рассчитывается на основе региона, настроек модема и этого поля. При значении 0 интервал автоматически рассчитывается на основе названия основного канала и изменяется с публичного интервала по умолчанию. Вернитесь к публичному интервалу по умолчанию, если настроены частный основной и общедоступный дополнительный каналы. - Задает максимальное количество прыжков, по умолчанию - 3. Увеличение количества также увеличивает перегрузку и должно использоваться с осторожностью. Сообщения с 0 прыжков не будут получать подтверждения. + 4/%1$d + Переопределение скорости кодирования + В этом пресете уже используется максимальная скорость кодирования. + Добавляет коррекцию ошибок поверх пресета. Более высокая скорость кодирования увеличивает длительность передачи каждого пакета и расходует больше рабочего цикла и ресурсов канала. + Пресет по умолчанию (%1$s) Пресеты этого региона предназначены только для лицензированных операторов (любительское радио). Включи «Лицензированный радиолюбитель (Ham)» в настройках пользователя, чтобы выбрать их. Доступные пресеты модема, по умолчанию - Long Fast. Регион, в котором вы будете использовать ваше радио. - Включение Ethernet отключит Bluetooth-соединение с приложением. TCP-соединения не доступны на устройствах Apple. Включить вещание пакетов через UDP в локальной сети. - Включение WiFi отключит Bluetooth-подключение к приложению. - Максимальное время между передачами позиции нодой. - Минимальное расстояние для умной рассылки координаты. - Чем меньше расстояние, тем быстрее будет отправляться обновление позицию. - Необязательные поля в сообщении о местоположении. Чем больше полей включено, тем больше сообщение, а значит больше время передачи и риск потери пакетов. - Как часто запрашивать координаты GPS (<10 секунд держит GPS включенным постоянно). - Все компоненты устройства будут засыпать, насколько возможно. Для ролей TRACKER или SENSOR также будет засыпать радиопередатчик. Не включай режим, если используешь ноду с телефоном или у ноды нет кнопки для пробуждения. Нода перезапускается и будет недоступна короткое время. - Открытый ключ для администрирования данной ноды - Вывод журнала отладки по последовательному каналу, просматривайте и выгружайте журналы устройства с измененным местоположением по Bluetooth. - Устройство управляется сетевым администратором, пользователь не может изменять настройки устройства. Используется для создания общего ключа с удаленным устройством. Устройство не передаёт свой приватный ключ через удалённое администрирование. Вы можете задать новый ключ, но его невозможно будет прочитать обратно. Сгенерировано из твоего приватного ключа и отправлено другим нодам сети, чтобы они могли вычислить совместный секретный ключ. - Последовательная консоль через Stream API. Настройки Настроить разрешения Bluetooth Настроить критические оповещения @@ -374,7 +333,6 @@ Фильтр включен Готовые фильтры Фильтры - API журнала отладки включен Нет журналов приложения Обновить Выгрузить логи @@ -425,8 +383,6 @@ Подробности Датчик обнаружения Настройка датчика обнаружения - Датчик определения включен - Тип триггера обнаружения Устройство Настройки устройства @@ -441,17 +397,15 @@ Интервал передачи %1$s %1$s: %2$s%% - Интервал обновления метрик устройства %1$s: %2$s В Устройство спит Хранилище устройства и UI (только для чтения) - Отправлять телеметрию устройства - Включите/выключите модуль телеметрии устройства, чтобы отправлять показатели в сеть. Это номинальные значения. Перегруженные сети будут автоматически масштабироваться на более длительные интервалы в зависимости от количества подключенных нод. Тема: %1$s, Язык: %2$s Точка росы Прямое сообщение Ключ прямого сообщения Личные сообщения + Включено Отмена Отключиться Отключено @@ -486,6 +440,7 @@ Сессия завершена Сканирование не удалось: %1$s История сканирования + Сканирование пресетов LoRa… Сессия не завершена Прогресс сканирования Суммарный скан @@ -528,22 +483,17 @@ Остановить сканирование Анализ ИИ недоступен Осталось %1$s - %1$d уникальных нод Посмотреть карту Свободно на диске %1$d - Дисплей Дисплей устройства - Режим экрана - Если включено, устройство будет отображать время на экране в 12-часовом формате. - Система измерения + Растворённый O₂ Расстояние Фильтр Расстояния Фильтровать список нод и сеть на основе близости к вашему телефону. Измерения расстояния Показать расстояния между вашим телефоном и другими нодами Meshstatic с позициями. - Служба доменных имен (DNS) Очистить условия поиска system ai,gemini,assistant,functions,automation,voice @@ -559,6 +509,7 @@ mqtt,broker,internet,bridge,uplink,downlink metrics,telemetry,signal,snr,rssi,battery,traceroute node,mesh,list,role,status,favorite,filter + notification,alert,sound,mute,reply,watch,wear os setup,welcome,permissions,first-launch module,serial,telemetry,canned,store-forward,administration settings,radio,lora,region,modem,device,power,security @@ -587,6 +538,7 @@ MQTT Метрики ноды Ноды + Уведомления Начало работы Настройки - Модули и администрирование Настройки - Радио и пользователь @@ -599,18 +551,18 @@ Документалка Готово Больше не показывать для этого устройства - Двойное нажатие как кнопка MQTT Downlink включён Сообщения от публичного интернет шлюза пересылаются в локальную сетку. Из-за политики нулевого хоста, трафик сервера MQTT по умолчанию не будет распространяться дальше, чем это устройство. Скачать Загрузить этот регион Обнаружен дубликат открытого ключа + Динамический Легко создать частные сети для защищённой и надежной связи в удаленных районах. - Echo включен Редактировать Редактировать источник сетевых плиток 8 часов + Электропроводность Активности Животные и природа @@ -624,28 +576,19 @@ Не удалось загрузить эмодзи Эмодзи не найдены Недавно использованные - Включить режим энергосбережения Включено - - Шифрование включено Несоответствие публичного ключа Открытый ключ не соответствует записанному ключу. Вы можете удалить ноду и позволить ей снова обменяться ключами, но это может указывать на серьезную проблему с безопасностью. Свяжитесь с пользователем по другому надежному каналу чтобы определить, произошла ли смена ключа в результате сброса настроек или другого преднамеренного действия. Общий ключ шифрования Для этой ноды есть открытый ключ, поэтому взаимодействие с ним происходит с использованием шифрования с открытым ключом. Метрики окружения - Окружающая среда - Модуль метрик окружения включен - Показатели окружения на экране включены - Интервал обновления метрик среды - Использовать метрику окружения в Fahrenheit Ошибка Достигнут лимит отправки сообщений в единицу времени. Не удается отправить сообщения прямо сейчас, пожалуйста, повторите попытку позже. Не удалось установить стабильное соединение после нескольких попыток. Пожалуйста, выберите ноду снова, чтобы попробовать ещё раз. Подключение и управление Установка удалённой сессии… Настройки Ethernet - Ethernet включен Ethernet IP: Работает событийная прошивка Использовать тему событий @@ -661,7 +604,6 @@ Экспорт пакета данных TAK Внешние уведомления Настройка внешнего уведомления - Внешние уведомления включены Сброс до заводских настроек Средний Meshtastic %1$s @@ -673,9 +615,9 @@ Добавьте слово или регулярное выражение:шаблон Отключить фильтрацию - Включить фильтрацию + Фильтровать все диалоги Включить фильтрацию - Скрывать сообщения, содержащие слова-фильтры + Скрывать сообщения, содержащие фильтры в каждом канале и ЛС. Выключает их для одного разговора из меню. Скрыть %1$d отфильтрованных Фильтр Отфильтрованные @@ -691,6 +633,11 @@ Версия прошивки %1$s завершен. Вернитесь к стандартной Meshtastic прошивке, чтобы восстановить нормальные функции. Обновление прошивки + Последнее: %1$s + Установлено: %1$s + Последняя версия: %1$s. Установленная версия отображается после перезагрузки устройства в режиме обновления. + Загрузчик обновлён + Обновлять нечего. Далее переустанавливается прошивка, что приведёт к перезагрузке устройства. Файл стирания был скопирован, но устройство не начало стирание. Пока ничего не изменилось. Отключи устройство, дважды нажми кнопку сброса и повтори попытку. Не удалось скопировать файл на диск устройства. Убедите, что диск всё ещё подключён, и повтори попытку. Список образов для стирания сейчас недоступен. Проверь подключение и повтори попытку или используй веб-флешер на flasher.meshtastic.org. @@ -742,6 +689,7 @@ Прошивка устройства, подождите... Держитесь крепче, работаем... Хэш прошивки отклонен. Устройство может потребовать подготовки хэша или обновления загрузчика. + Обновление прошивки… Выбранный файл "%1$s" не соответствует %2$s (%3$s). Выберите файл прошивки для этой цели. Держите устройство поближе к телефону. Обновить до: %1$s @@ -784,6 +732,7 @@ Это может занять минутку... Целевое устройство: %1$s Обновление прошивки + %1$d% Неизвестная ошибка Неизвестная модель оборудования: %1$d Неизвестная внешняя версия @@ -804,9 +753,7 @@ Очистить устройство при обновлении Полностью стирает флэш-память устройства, а затем устанавливает выбранную прошивку с нуля. Версия прошивки: %1$s - Фиксированный PIN-код Фиксированная позиция - Повернуть экран Дополнительная информация доступна в нашей политике конфиденциальности. Полужирный @@ -817,13 +764,7 @@ Свободная память Доступная оперативная память в байтах Частота - Частота слота - Понятное имя Сопротивление газа - Шлюз - Создать событие ввода для CCW - Создать событие ввода для CW - Создать событие ввода при нажатии Сгенерировать QR-код Геозона @@ -848,32 +789,18 @@ Начать работу Репозиторий GitHub Хороший - GPIO Контакт GPIO - Контакт GPIO для порта A поворотного энкодера - Контакт GPIO для порта B поворотного энкодера - Контакт GPIO для порта Press поворотного энкодера - GPIO контакт для мониторинга - GPIO EN GPS - Режим GPS (физическое оборудование) - GPIO приёма GPS - GPIO передачи GPS Дать разрешение - Зеленый Необязательно. Добавляется к твоему позывному, например, KD2ABC//Attic Heltec Оборудование Модель оборудования Курс - Heartbeat Справка и документация Скрыть слой Скрыть пароль - Макс возврат истории - Окно возврата истории В этом окне нет нод Нод на хоп - Количество прыжков Прыжков Хост Метрики хоста @@ -881,18 +808,12 @@ Я согласен. Я прочитал и понял вышеописанное. Я добровольно даю согласие на незашифрованную передачу данных моей ноды через MQTT Я знаю, что делаю. - I2S Clock - I2S Data In - I2S Data Out - I2S Word Select Относительное качество воздуха в помещении (Качество воздуха в помещении) Относительная шкала IAQ, измеренная Bosch BME680. Диапазон значений 0–500. Обозначение значков - Игнорировать Добавить '%1$s' в список игнорируемых? Игнорировать входящие - Игнорировать MQTT Удалить '%1$s' из списка игнорируемых? Импорт настроек @@ -920,7 +841,6 @@ IP-адрес IP-адрес: Порт: - Режим IPv4 Вывод JSON включен %1$s %1$s +%2$d @@ -933,6 +853,7 @@ Проверка ключа завершена Запрос проверки ключа Проверка ключа + %1$s (%2$s) Фильтр по времени последнего сообщения: %1$s Обновление последнего местоположения Последняя альфа @@ -943,10 +864,10 @@ Оценка покрытия Сетевой уровень Подробности - Сердцебиение светодиодом - Состояние LED Устаревший канал Администратора %1$d библиотек + Лицензия + Свободное программное обеспечение под лицензией GNU General Public License v3, без каких-либо гарантий. Ты можешь распространять его на условиях той же лицензии. Лицензия радиолюбителя (HAM) Включение данной опции отключает шифрование и несовместимо с основной сетью Meshtastic. @@ -954,6 +875,8 @@ Включить лицензированный (радиолюбительский) режим? Лицензированный режим удаляет ключи шифрования каналов и отключает канал администратора, поэтому весь трафик передаётся в открытом виде, который может прочитать кто угодно, а эта прошивка не может его подписывать, поэтому другие ноды не смогут подтвердить, что сообщение исходит от тебя. Это несовместимо со стандартной сетью Meshtastic, и вы остаётесь ответственны за соблюдение требований вашей радиолюбительской лицензии и местных нормативов. Лицензированный режим удаляет ключи шифрования каналов и отключает канал администратора, поэтому весь трафик передаётся в открытом виде, который может прочитать кто угодно, но он имеет цифровую подпись, позволяя другим нодам убедиться, что сообщение исходит от тебя. Номер вашей ноды может однократно измениться, чтобы совпасть с твоим ключом идентификации, поэтому избранное, история сообщений, удалённое администрирование и другие ноды могут поначалу воспринять её как новую; вы остаётесь ответственны за соблюдение требований вашей радиолюбительской лицензии и местных нормативов. + Расстояние до грозы + Молнии (за 1 ч) Загрузка Загрузка 15 мин @@ -999,6 +922,7 @@ Введи пароль для отключения блокировки. Устройство расшифрует хранилище и перезагрузится. Включить блокировку Понимаю + Включение блокировки запирает отладочный порт (SWD) на оборудовании, где эта блокировка действует. Последующее отключение блокировки расшифровывает твои данные, но порт остаётся запертым. Чтобы снова открыть его, потребуется полное стирание чипа с помощью отладочного программатора, что уничтожит всё на устройстве. Введи парольную фразу Скрыть Часов до истечения @@ -1049,13 +973,10 @@ Освещённость Управление собственными источниками плиток Управление Слоями Карты - Управляемый режим Требуется запрос позиции вручную Карта сети - Емкость кэша: %1$d MB\nИспользование кэша: %2$d MB Менеджер кэша - %1$s МБ Текущий размер кэша %1$d файла Очистить загруженные файлы @@ -1098,11 +1019,8 @@ Метеорадары Ошибка очистки кэша SQL, подробности в logcat Кэш SQL очищен для %1$s - Отчёты по карте Согласие на передачу незашифрованных данных ноды через MQTT Включая данную функцию, вы подтверждаете и прямо соглашаетесь на передачу географического местоположения вашего устройства в реальном времени через протокол MQTT без шифрования. Эти данные о местоположении могут быть использованы для таких целей, как отчёты карты в реальном времени, отслеживание устройства и подобные функции телеметрии. - Интервал отчета карты (в секундах) - Ваша нода будет периодически отправлять незашифрованный пакет отчёта карты на настроенный MQTT-сервер, что включает ID, полное и краткое имя, примерное местоположение, модель аппаратного обеспечения, роль, версию прошивки, регион LoRa, режим работы передатчика и имя основного канала. Выберите регион загрузки Начать скачивание Выбор стиля карты @@ -1123,6 +1041,7 @@ Маяк сети Трансляция маяка + Трансляция маяка объявляет пресет модема, чтобы другие могли присоединиться. Это радио использует собственные настройки LoRa вместо стандартного пресета, поэтому маяк может пригласить людей на настройки, которые они не смогут услышать. Здесь можно отключить трансляцию, но не включить её, пока радио не начнёт использовать стандартный пресет модема. Периодически рекламировать эту сеть на близлежащие ноды Канал %1$d Каналы маяка @@ -1131,7 +1050,6 @@ Приглашения в сеть Слушать маяки Захват приглашений, рекламируемых ближайшими сетями - Сообщение маяка Максимум %1$d байт Нет доступных каналов Ближайшая сеть пригласила тебя присоединиться @@ -1151,6 +1069,7 @@ Рекламируется ближайшим маяком Отсутствует Регион / Страна + Укажите регион твоего радио перед настройкой маяка. Цель %1$d Добавить цель Канал @@ -1162,8 +1081,11 @@ Meshtastic Служебные уведомления + Критические оповещения, отправляемые нодами в сеть. Meshtastic Уведомления о сообщениях в общем чате + Сообщения, отправляемые в ваши каналы. + Уведомления по радио Уведомление о низком уровне заряда Уведомления о низком заряде батареи (избранные ноды) Уведомления о приглашениях в сеть @@ -1207,10 +1129,12 @@ Подтверждение не было получено вовремя. Повтори попытку, когда улучшится сигнал или покрытие mesh-сети. Слишком большое сообщение для отправки Сократи сообщение и отправь его снова. + Сбой проверки доставки Отправлено в сеть Отправка... В очереди на отправку Доставлено получателю + Доставлено получателю - доказано Передано, не подтверждено получателем Подтверждено в цепочке SF++ Маршрутизация по SF++ цепочке… @@ -1220,9 +1144,6 @@ %1$s %2$d µg/m³ Мин - Минимальная трансляция (в секундах) - Умное расстояние - Умный интервал Минимальное время бодрствования Шаблоны Настройки модуля @@ -1232,7 +1153,6 @@ MQTT Настройка MQTT - MQTT включен MQTT: соединение потеряно MQTT: соединение отклонено (проверьте учетные данные) Сбой MQTT-прокси: %1$s @@ -1246,8 +1166,6 @@ Не удается подключиться к брокеру (TCP) Тайм-аут после %1$d мс Ошибка TLS-рукопожатия: %1$s - MQTT-прокси на этом телефоне - Этот телефон передаёт MQTT-трафик для подключённого устройства. Выключите, чтобы сразу прекратить передачу, не изменяя настройки MQTT устройства — полезно, когда трафик MQTT перегружает соединение. Включите обратно, чтобы продолжить передачу. Подключено Подключение… Отключено @@ -1255,6 +1173,8 @@ Неактивно Восстановление связи… Восстановление (попытка %1$d) — %2$s + Подключено, но брокер отклонил все темы: %1$s + Подключено, но брокер отказал %1$s Проверить соединение Вы должны задать регион! Вам необходимо обновить данное приложение в магазине приложений (или с Github). Оно слишком старо для взаимодействия с прошивкой радиостанции. Пожалуйста, прочитайте нашу документацию по этой теме. @@ -1270,7 +1190,6 @@ Обеззвучен на %1$d дней, %2$s часов Обеззвучен на %1$s часов Не заглушен - Таймаут Nag (в секундах) Имя Имя не может быть пустым. Вернуться @@ -1280,7 +1199,6 @@ Meshtastic требует совместимое устройство. Наши спонсоры и партнёры предлагают готовое к использованию оборудование. Вот некоторые из самых популярных вариантов тут. Информация об окружении Настройки соседей - Информация о соседях включена Сеть Получен URL нового канала Новые сообщения ниже @@ -1288,12 +1206,13 @@ Новые ноды Далее NFC отключен. Пожалуйста, включите его в настройках вашего устройства. + Нитрат + Азот Убедитесь, что вы находитесь в зоне действия устройства. Bluetooth-устройства не обнаружены Источники пользовательских плиток не найдены. Устройство не выбрано - Устройства не найдены Файлы не отобразились. Статистика недоступна Слои карты не загружены. @@ -1302,6 +1221,7 @@ Сетевые устройства не обнаружены Метрики прохожих недоступны Нет открытого ключа + Эта нода не передала открытый ключ, поэтому зашифрованное взаимодействие с ней невозможно. Запроси информацию о пользователе, чтобы запросить ключ. Устройства USB не найдены Подключи устройство с помощью USB-кабеля для передачи данных, чтобы использовать последовательное соединение. USB-устройства не обнаружены @@ -1312,10 +1232,13 @@ %1$d всего Исключить инфраструктуру Исключить MQTT + Скрыть ноды без сигнала Вы просматриваете игнорируемые ноды,\nНажмите, чтобы вернуться к списку всех нод. Включить неизвестные Отображать только слышимые ноды + Только зашифрованные Скрыть ноды офлайн + Только подписанные Фильтр Показать только игнорируемые ноды Фильтр по @@ -1329,6 +1252,11 @@ Полная схема отображает все доступные данные нод. Поля без данных автоматически скрываются. Роль устройства Расстояние и азимут + Сигнал слабый. SNR на 5,5–7,5 дБ ниже предела SNR пресета. + Сигнал средний. SNR менее чем на 5,5 дБ ниже предела SNR пресета. + Сигнал сильный. SNR выше предела SNR вашего пресета модема. + Оценивает SNR относительно предела SNR твоего пресета модема и показывает результат в виде цветного значка с подписью. RSSI отображается, но не влияет на оценку. + Сигнал непригоден. SNR более чем на 7,5 дБ ниже предела SNR пресета. Прыжков Время последнего вещания Метрики окружения @@ -1343,6 +1271,7 @@ Детали ноды Справка по списку нод Параметры ноды + Не слышно на твоих текущих настройках LoRa Номер ноды Не удалось отправить запрос. Повторите попытку. Перезапуск… @@ -1356,7 +1285,6 @@ по избранным через MQTT Очистка списка нод сети - Интервал вещания передачи информации об узле Ноды Ноды в этом месте @@ -1365,6 +1293,9 @@ Ближайшие узлы будут появляться здесь по мере их обнаружения. Поиск узлоов %1$d нод в очереди для удаления: + %1$d нод не слышно твоих текущих настройках LoRa + 1 нода не слышна на твоих текущих настройках LoRa + Оставить Удалить Уровень шума Фоновый радиошум, измеряемый радиоприемником. Более низкие значения обычно указывают на более тихое окружение для приемника; -85 дБм — это ориентировочная отметка загруженности, а не жесткий порог отказа. @@ -1375,6 +1306,11 @@ Не сейчас Примечание Заметки + + Меш + Уведомления отключены, и Android больше не будет спрашивать. Включи их в настройках приложения, чтобы получать оповещения о новых сообщениях и тревогах. + Без уведомлений Meshtastic не сможет оповещать тебя о новых сообщениях, новых нодах или низком заряде батареи, когда приложение работает в фоновом режиме. + Уведомления — это способ, которым Meshtastic связывается с тобой, когда приложение закрыто(в фоне): новые сообщения, обнаруженные ноды и низкий заряд батареи радио. Если ты откажешься, больше ничего не изменится. Meshtastic использует уведомления, чтобы держать вас в курсе новых сообщений и других важных событий. Вы можете обновить разрешения уведомлений в любое время из настроек. Уведомления для каналов и личных сообщений. @@ -1383,13 +1319,14 @@ Уведомления при получении оповещения/звонка Уведомления о получении сообщения Только что - NTP-сервер - Количество записей Офлайн-карты - ОК в MQTT + Карты ещё не загружены + %1$s: %2$s · %3$s + Рельеф ещё не загружен + Офлайн-рельеф + Включает детализированный рельеф региона Лады - Тип OLED 24 часа 1 час @@ -1408,23 +1345,15 @@ Открыть настройки Wi-Fi Опции Ориентация на север - - Выход Буззера (GPIO) - Продолжительность вывода (миллисекунды) - Вывод светодиода активный высокий - Выход LED (GPIO) - Вибросигнал (GPIO) + ОВП Переполнение меню Переопределить COM-порт консоли - Переопределить рабочий цикл - Переопределить частоту - PA вентилятор выключен Подлинность пакета Сбалансировано — предпочтительно аутентифицировать Рекомендуется. Отклонять неподписанные попытки понижения от нод, которые, как известно, подписывают. Совместимо — принять не подписано - Аутентифицировать пакеты по возможности, но принимать неподписанный трафик для максимальной совместимости. + Для максимальной совместимости принимать трафик без цифровой подписи. Пакет все равно будет отброшен, если подпись неверна и ее возможно проверить. Уровень защиты Строгий — требуется аутентификация Включить строгий @@ -1432,7 +1361,6 @@ Показывать и обрабатывать только криптографически аутентифицированные mesh-пакеты. Старые ноды и слишком большие пакеты могут исчезнуть. Включить строгую аутентификацию? Это подключенное устройство не поддерживает верификацию подписи пакетов. - Режим сопряжения Пароль PAX @@ -1445,9 +1373,12 @@ Wi-Fi: %1$s Счётчик прохожих Настройки Paxcounter - Paxcounter включен Периодическое вещание позиции + Сканирование QR-кодов каналов и контактов + Поиск радиоустройств в твоей сети Wi-Fi + Показывать твоё местоположение на карте и делиться им с mesh-сетью + Найди своё радио по Bluetooth, отметь себя на карте и поделите своим местоположением с mesh-сетью Meshtastic требует разрешение на поиск и подключение к устройствам через Bluetooth. Вы можете отключить его, когда он не используется. В этой версии Android одно разрешение на местоположение охватывает две вещи: оно позволяет сканировать Bluetooth и отображает тебя на карте. Совместное использование твоей позиции остаётся выключенным, пока ты сам его не включишь. Найти и подключиться к ноде через Bluetooth @@ -1471,6 +1402,7 @@ Показать разрешения Местоположение телефона Meshtastic использует местоположение вашего телефона, чтобы включить ряд функций. Вы можете обновить права доступа к вашему местоположению в любое время из настроек. + Фосфор PIN-код Воспроизвести @@ -1479,6 +1411,12 @@ %1$d часа %1$d часы + + %1$d миллисекунда + %1$d миллисекунды + %1$d миллисекунд + %1$d миллисекунд + %1$d минута %1$d минуты @@ -1491,6 +1429,16 @@ %1$d секунд %1$d секунд + + Состояние датчиков + Ошибка CO₂ + Ошибка вентилятора + Предупреждение о скорости вентилятора + Ошибка датчика газа + Ошибка HCHO + Ошибка датчика PM + Ошибка RH&T + Неизвестные флаги 0x%1$s PM1.0 PM10 PM2.5 @@ -1498,16 +1446,12 @@ Местоположение Установить местоположение с телефона Местоположение включено - Флаги позиции Местоположение Пакет позиции - + Калий Питание Настройка питания Метрики питания - Модуль метрик питания включен - Включить метрики питания на экране - Интервал обновления метрик электропитания Питание ppm Точность местоположения @@ -1529,13 +1473,10 @@ Текст Первичный Периодическая трансляция местоположения и телеметрии - Приватный ключ Информация о проекте Предоставление местоположения для сети Имя провайдера уже существует. - Прокси клиенту включен Пароль - Контакт PTT Публичный ключ Публичный ключ изменён QR-код @@ -1553,25 +1494,14 @@ Дождь (24ч) Проверка дальности Настройка проверки дальности - Проверка дальности включена Среагировать Перезагрузка - - Режим ретрансляции - Ретранслировать замеченное сообщение, если оно было на нашем частном канале или из другой сетки с теми же параметрами lora. - Так же, как и ALL, но пропускает декодирование пакетов и просто ретранслирует их. Доступно только в роли Repeater. Установка этого параметра для любых других ролей приведет к изменению поведения ALL. - Игнорирует пакеты из нестандартных портов, таких как: TAK, RangeTest, PaxCounter и т. д. Только ретранслирует пакеты со стандартными номерами портов: NodeInfo, Text, Position, Telemetry, Routing. - Игнорируемые сообщения из других сетей, таких как LOCAL ONLY, но так же, и игнорирует сообщения от узлов, которые еще не включены в известный список узлов. - Игнорирует обнаруженные сообщения из чужих mesh-сетей, которые открыты или не могут быть расшифрованы. Ретранслирует сообщение только на локальных основных / дополнительных каналах нод. - Разрешено только для ролей SENSOR, TRACKER и TAK_TRACKER, это запретит все ретрансляции, не похожие на роль CLIENT_MUTE. Недавние сетевые устройства Восстановление связи… - Красный Обновить Обновить метаданные Вы уверены, что хотите пересоздать свой приватный ключ?\n\nНоды, которые ранее обменивались ключами с этой нодой, должны будут удалить её и повторно обменяться ключами для того, чтобы возобновить защищённую связь. Пересоздать приватный ключ - Регион / Страна Услышано %1$d ретранслятором Услышано %1$d ретрансляторами @@ -1625,32 +1555,17 @@ Роль устройства Client Client Base - Обрабатывает пакеты от избранных нод как ROUTER_LATE, а все остальные пакеты - как от CLIENT. - Приложение подключено или автономное устройство обмена сообщениями. Client Hidden - Устройство, которое передает сигнал только при необходимости для скрытности или экономии энергии. Client Mute - Устройство, которое не пересылает пакеты с других устройств. Lost and Found - Регулярно отправляет местоположение в виде сообщения в основной канал, чтобы помочь с восстановлением устройства. Repeater - Инфраструктурная нода для расширения покрытия сети путем передачи сообщений с минимальными накладными расходами. Не видна в списке нод. Router Router Client - Сочетание ROUTER и CLIENT. Не для носимых устройств. - Инфраструктурная нода для расширения охвата сети путем передачи сообщений. Видима в списке нод. Router Late - Инфраструктурная нода, которая всегда ретранслирует пакеты один раз, но только после всех остальных режимов, обеспечивая дополнительное покрытие для локальных кластеров. Видима в списке. Sensor - Транслирует пакеты телеметрии в приоритетном порядке. Тактический - Оптимизировано для связи с системой ATAK, сокращает текущие передачи. TAK Tracker - Включает автоматические трансляции TAK PLI и сокращает рутинные трансляции. Tracker - Транслирует пакеты местоположения GPS в приоритетном порядке. - Корневая тема - Поворотный энкодер #1 включен Я прочитал <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Документацию по роли устройств</a> и пост в блоге о <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Выборе верной роли устройства</a>. Сеанс администратора истёк @@ -1673,13 +1588,11 @@ Слишком большое сообщение для отправки RSSI Индикатор уровня принимаемого сигнала, измерение, используемое для определения уровня мощности, принимаемой антенной. Более высокое значение RSSI обычно указывает на более сильное и стабильное соединение. - Сервер rsyslog + Солёность Количество спутников - Сохранить Сохранить и перезапустить Сохранить - Сохранить .CSV в хранилище (только ESP32) Экспортировать пакеты для проверки дальности Сканирования @@ -1693,7 +1606,6 @@ Сканировать QR-код контакта Поиск... Поиск... - Включать экран на Прокрутить вниз Поиск эмодзи... Поиск сообщений… @@ -1702,6 +1614,12 @@ Нет периодической телеметрической передачи Безопасность + Сбой проверки доставки + Что-то признало это сообщение без ключа, только зацепки получателя. + Доказательство доставки не проверено + Доказательство доставки прибыло, но не было ключа для его проверки. + Доставка проверена + Упомянутая вами нода подтвердила, что она получил это сообщение. Предупреждающий Знак Статус безопасности Отменить @@ -1726,36 +1644,33 @@ Радио проверило подписанные трансляции этой ноды с помощью её ключа идентификации, так что его личность постоянна, но ты сам её не проверял. Каждая нода с прошивкой 2.8 или выше подписывает свои трансляции. Подписано · проверено Подтверждённый контакт + Ты лично подтвердил ключ этой ноды, обменявшись QR-кодами контактов, поэтому её подлинность подтверждена. Это наивысший уровень доверия, отображаемый в списке, и он показывается на любой версии прошивки. Выбрать Выбрать все Выбрано Выбранный тип карты Отправить - Отправить колокольчик - Отправить колокол с уведомлением - Интервал сообщений отправителя (в секундах) - COM-порт - Скорость COM-порта Настройка COM-порта - Консоль COM-порта - COM-порт включен - Режим COM-порта - RX - TX - Сервер Сессия активна Требуется обновление Установить время Настроить соединение Установите ваш регион настройки + Нет настроек, соответствующих этой + Настройки поиска Поделиться Поделиться ссылкой + Пока это открыто, другой телефон также может принять приглашение, коснувшись твоего. Поделиться каналами + Добавление сохраняет каналы, которые у них уже есть, и добавляет твои. + Замена перезаписывает все каналы на их радио твоими. + Каналы Meshtastic Поделиться подключенной нодой Отправить контакт + Контакт Meshtastic Поделиться геопозицией Используй GPS твоего телефона для отправки координат на твою ноду вместо аппаратного GPS на самой ноде. Отсканируй этот код с помощью приложения Meshtastic на другом устройстве, чтобы добавить его туда. @@ -1778,7 +1693,6 @@ Показать путевые точки Выключение Узел: %1$s - Выключение при потере мощности ⚠️ Эта нода будет ВЫКЛЮЧЕНА. Для её включения потребуется физическое взаимодействие. Сигнал Качество сигнала @@ -1812,47 +1726,34 @@ Системный WebView обновляется. Повтори попытку через мгновение. Пропустить Слот - Умная позиция Сигнал/шум Соотношение сигнал/шум, мера, используемая в коммуникациях для количественной оценки уровня желаемого сигнала по отношению к уровню фонового шума. В Meshtastic и других беспроводных системах более высокий SNR указывает на более четкий сигнал, который может повысить надежность и качество передачи данных. Влажн почвы + pH почвы Темп почвы + Почва и вода + Солнечная облучённость Скорость %1$d км/ч %1$d миль/ч - Коэффициент распространения - Название сети - Трансляция состояния (в секундах) Состояние сообщения Публичное сообщение о статусе, которое отправляется в сеть при изменении и каждые 12 часов. Оставайтесь на связи везде Остановить подключение Store & Forward Настройка Store & Forward - Store & Forward включена - Подсеть Успешно Длительность супер-глубокого сна Поддерживается Поддерживается Meshtastic Community + Оборудование независимых производителей Удалить Заглушить Включить микрофон - Усиление RX Настройка системы TAK (ATAK) Настройка TAK - Роль участника - Вперёдсмотрящий - Штаб-квартира - Собака (К9) - Санитар - Оператор радиотелефона - Снайпер - Руководитель команды - Участник команды - Не определена Сервер TAK Меш-канал TAK Канал Meshtastic используется для исходящего трафика TAK @@ -1889,22 +1790,6 @@ Запустить Запущено: %1$s Прошивка подключённой ноды не поддерживает полную интеграцию с TAK — в ATAK будут передаваться только сообщения о местоположении и чате. Маркеры и другие типы событий требуют прошивки 2.8.0 или новее. - Цвет команды - Синий - Коричневый - Голубой - Тёмно-синий - Тёмно-зеленый - Зеленый - Пурпурный - Бордовый - Оранжевый - Фиолетовый - Красный - Бирюзовый - Не указан - Белый - Жёлтый Телеметрия Настройка телеметрии Темп @@ -1913,10 +1798,8 @@ Светлая По умолчанию Время - Часовой пояс Время ожидания истекло Отметка времени - TLS включен Переключить мою позицию Трассировка маршрута @@ -1970,7 +1853,6 @@ Сообщение уже на вашем языке Передача отключена Это устройство может принимать данные, но ничего не отправляет через LoRa. - Передать через LoRa Транспорт API @@ -1981,15 +1863,17 @@ UDP USB Маякнуть при тройном нажатии + Мутность 24ч 48 часов 2нед - Передача включена - Мощность передатчика Тип Написать сообщение UDP трансляция Отменить + дБм + кГц + м Единицы измерения По умолчанию @@ -2010,9 +1894,6 @@ Открепить Нераспознанный Не задано - 0 - Вверх/Вниз/Выбирать включён - Интервал опроса GPS - Интервал обновления (в секундах) Обновить статус Обновлено MQTT Uplink включён @@ -2026,13 +1907,7 @@ Шаблон URL USB Доступ к USB запрещён. Подключи устройство снова, чтобы попробовать ещё раз. - - Использовать 12-часовой формат времени Компактная кодировка кириллицы - Использовать I2S как буззер - Использовать режим INPUT_PULLUP - Использовать шаблон - Использовать PWM-звукоизлучатель Пользователь Настройки пользователя @@ -2040,7 +1915,6 @@ Пользовательская информация Строка пользователя Пользовательская информация - Имя пользователя УФ Люкс через API через MQTT @@ -2048,9 +1922,8 @@ Показать на карте Просмотреть релиз Напряжение - Длительность ожидания Bluetooth - Включать экран при касании или движении Предупреждение + pH воды Удалить путевую точку? Редактировать путевую точку Установить путевую точку @@ -2062,7 +1935,6 @@ Параметры Wi-Fi Настройка Wi-Fi для mPWRD-OS - Wi-Fi включен Wi-Fi IP: Доступные сети Не удалось подключиться: %1$s @@ -2098,7 +1970,6 @@ Настройка Wi-Fi для mPWRD-OS Неверный формат QR-кода Wi-Fi Сканировать QR-код WiFi - Порог WiFi RSSI (по умолчанию -80) Нет подключения к Wi-Fi. Сканирование сети может не обнаружить ближайшие устройства. Ветер diff --git a/core/resources/src/commonMain/composeResources/values-sk/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-sk/schema_strings.xml new file mode 100644 index 0000000000..02d919aa4c --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-sk/schema_strings.xml @@ -0,0 +1,116 @@ + + + + + Modrá + Prúd + Zelená + Stav LED + Červená + Predvolené + CODEC2 vzorkovacia frekvencia + CODEC 2 zapnutý + I2S výstup dát + I2S čas + I2S vstup dát + I2C výber slova + PTT pin + Bluetooth zapnuté + Režim párovania + GPIO konektor pre Enkóder A port + GPIO konektor pre Enkóder B port + Odmietnuť + Žiadny + Enkóder #1 aktivovaný + Názov + Vykoná dvojklepnutie na podporovaných akcelerometroch ako stlačenie užívateľského tlačidla. + Všetky + Preskočiť Dekódovanie Všetkých + Rovnaké ako správanie ako ALL, ale preskočí dekódovanie paketov a jednoducho ich prepošle. Dostupné iba v úlohe Opakovača. Nastavenie tejto možnosti na akékoľvek iné roly bude mať za následok správania sa ako ALL. + Iba Známe + Iba Lokálne + Ignoruje pozorované správy z cudzích sietí, ktoré sú otvorené alebo tie, ktoré nedokáže dešifrovať. Opätovne vysiela správu iba na lokálnych primárnych / sekundárnych kanáloch uzlov. + Žiadny + Povolené len pre role SENSOR, TRACKER a TAK_TRACKER, zamedzí to všetkým opätovným vysielaniam, na rozdiel od roly CLIENT_MUTE. + Klient + Skrytý Klient + Zariadenie, ktoré vysiela len podľa potreby pre utajenie, alebo úsporu energie. + Stlmený Klient + Zariadenie, ktoré nepreposiela pakety z ďalších zariadení. + Straty a nálezy + Pravidelne vysiela polohu ako správu na predvolený kanál, aby pomohol pri obnove zariadenia. + Opakovač + Smerovač + Smerovač Klient + Smerovač s Oneskorením + Senzor + Prioritne vysiela telemetrické pakety. + TAK + Optimalizované pre systémovú komunikáciu ATAK, znižuje rutinné vysielanie. + TAK Sledovač + Umožňuje automatické vysielanie TAK PLI a znižuje rutinné vysielanie. + Sledovač + Prioritne vysiela pakety polohy GPS. + Otoč Obrazovku + Otočiť obrazovku vertikálne. + Doba počas ktorej ostane obrazovka zapnutá potom ako je stlačené používateľské tlačidlo alebo bola prijatá správa. + Šírka pásma + Ďaleký Dosah - Rýchlo + Ďaleký Dosah - Mierne + Ďaleký Dosah - Pomali + Ďaleký Dosah - Turbo + Stredný Dosah - Rýchlo + Stredný Dosah - Pomali + Krátky Dosah - Rýchlo + Krátky Dosah - Pomali + Krátky Dosah - Turbo + Veľmi Ďaleký Dosah - Pomali + Región + Správa + Adresa + Heslo + Používateľské meno + Vysielať cez sieť LoRa + Okrem odoslania do MQTT a PhoneAPI je prenášanie informácii o susedoch prostredníctvom LoRa. Nedostupné na kanáli s predvoleným kľúčom a názvom. + Aktualizačný interval + IPv4 režim + Ethernet zapnutý + NTP server + Žiadny + Heslo + SSID + Aktualizačný interval + Minimálna vzdialenosť + Minimálny Interval + Fixná Pozícia + Aktualizačný interval + Broadcastový Interval + Inteligentná poloha + Nadmorská výška + Časová značka + Uložiť + Súkromný kľúč + Verejný kľúč + Predvolené + Predvolené + Časový limit + Rola + Modrá + Zelená + Červená + diff --git a/core/resources/src/commonMain/composeResources/values-sk/strings.xml b/core/resources/src/commonMain/composeResources/values-sk/strings.xml index 7749cefa9b..818c733299 100644 --- a/core/resources/src/commonMain/composeResources/values-sk/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sk/strings.xml @@ -17,6 +17,8 @@ --> + Vlhkosť + Teplota O aplikácii Prijať @@ -28,7 +30,6 @@ Administrácia Percento vysielacieho času na prenos použitého za poslednú hodinu. - Znak zvončeku upozornení! Všetky Nadmorská výška @@ -45,17 +46,12 @@ Zvuk Konfigurácia zvuku Zlý - - Šírka pásma Batéria - Modrá Bluetooth Konfigurácia Bluetooth - Bluetooth zapnuté Bluetooth Nastavenia - Broadcastový Interval Prepočítavanie… Práva pre prístup k fotoaparátu Odmietnuť @@ -73,7 +69,6 @@ Kanál 7 Kanál 8 URL adresa tohoto kanála nie je platná a nedá sa použiť - Kanál Názov kanála Kanále @@ -82,16 +77,10 @@ Zmazať Zavrieť - CODEC 2 zapnutý - CODEC2 vzorkovacia frekvencia - Vykoná dvojklepnutie na podporovaných akcelerometroch ako stlačenie užívateľského tlačidla. Ovláda blikajúcu LED na zariadení. Pre väčšinu zariadení toto ovláda jednu zo štyroch LED, neovláda LED pre GPS a nabíjanie. - Okrem odoslania do MQTT a PhoneAPI je prenášanie informácii o susedoch prostredníctvom LoRa. Nedostupné na kanáli s predvoleným kľúčom a názvom. Použiť časovú zónu telefónu - Otočiť obrazovku vertikálne. - Doba počas ktorej ostane obrazovka zapnutá potom ako je stlačené používateľské tlačidlo alebo bola prijatá správa. Pripojený Pripojené k uspanému vysielaču Prebieha pripájanie @@ -139,24 +128,21 @@ Priamo Správy - Obrazovka Vzdialenosť Vymazať vyhľadávanie MQTT + Upraviť 8 Hodín - Nezhoda verejného kľúča Šifrovanie verejného kľúča - Prostredie Chyba Dosiahnutý limit pracovného cyklu. Nedá sa teraz posielať správy, skúste neskôr. - Ethernet zapnutý Eternet IP: Vymeniť si pozície @@ -173,30 +159,19 @@ Firmvér vysielača je príliš zastaralý, aby dokázal komunikovať s aplikáciou. Viac informácií nájdete na našom sprievodcovi inštaláciou firmvéru. Nutná aktualizácia firmvéru. Aktualizácia zlyhala - Pevný PIN Fixná Pozícia - Otoč Obrazovku Dobrý - GPIO GPIO konektor - GPIO konektor pre Enkóder A port - GPIO konektor pre Enkóder B port - Zelená Hardvér Model hardvéru Skryť heslo Počet skokov Viem čo robím. - I2S čas - I2S vstup dát - I2S výstup dát - I2C výber slova IAQ (Kvalita vzduchu v interiéri) relatívna hodnota IAQ meraná prístrojom Bosch BME680. Rozsah hodnôt 0–500. - Ignorovať Pridať '%1$s' do zoznamu ignorovaných? Odstrániť '%1$s' zo zoznamu ignorovaných? @@ -207,10 +182,8 @@ Zobraziť úvod IP adresa: Port: - IPv4 režim - Stav LED Zaťaženie %1$d @@ -240,7 +213,6 @@ Slabá batéria: %1$s Lux - Kapacita cache: %1$d MB\nVyužitie cache: %2$d MB Cache Manager Aktuálna veľkosť cache %1$d dlaždíc @@ -325,16 +297,14 @@ Žiaden (zakázať) Žiadny Nepripojené + - NTP server 24 Hodín 1 týždeň - - Režim párovania Heslo Počítadlo ľudí @@ -346,22 +316,20 @@ %1$d hodín %1$d hodín + Pozícia Zapnutá poloha Pozícia Paket Pozície - Napájanie Jazyk Predvolené nastavenie Tlak Primárna - Súkromný kľúč Poskytnúť polohu telefónu do siete PSK - PTT pin Verejný kľúč QR kód @@ -374,15 +342,6 @@ Konfigurácia vysielača Test dosahu Reštartovať - - Preposiela akúkoľvek pozorovanú správu, ak bola na našom súkromnom kanáli alebo z inej siete s rovnakými parametrami lora. - Rovnaké ako správanie ako ALL, ale preskočí dekódovanie paketov a jednoducho ich prepošle. Dostupné iba v úlohe Opakovača. Nastavenie tejto možnosti na akékoľvek iné roly bude mať za následok správania sa ako ALL. - Ignoruje pakety z neštandardných portov, ako sú: TAK, RangeTest, PaxCounter atď. Opätovne vysiela iba pakety so štandardnými portami: NodeInfo, Text, Position, Telemetry a Routing. - Ignoruje pozorované správy z cudzích sietí, ako napríklad LOCAL ONLY, ale ide o krok ďalej tým, že ignoruje aj správy z uzlov, ktoré ešte nie sú v známom zozname uzla. - Ignoruje pozorované správy z cudzích sietí, ktoré sú otvorené alebo tie, ktoré nedokáže dešifrovať. Opätovne vysiela správu iba na lokálnych primárnych / sekundárnych kanáloch uzlov. - Povolené len pre role SENSOR, TRACKER a TAK_TRACKER, zamedzí to všetkým opätovným vysielaniam, na rozdiel od roly CLIENT_MUTE. - Červená - Región Administrácia na diaľku Vzdialený hardvér @@ -398,29 +357,17 @@ Zvonenie Klient - Pripojená aplikácia, alebo samostatné zariadenie na odosielanie správ. Skrytý Klient - Zariadenie, ktoré vysiela len podľa potreby pre utajenie, alebo úsporu energie. Stlmený Klient - Zariadenie, ktoré nepreposiela pakety z ďalších zariadení. Straty a nálezy Opakovač - Uzol infraštruktúry na rozšírenie pokrytia siete prenosom správ s minimálnou réžiou. Nezobrazuje sa v zozname uzlov. Smerovač Smerovač Klient - Kombinácia ROUTER a CLIENT. Nie pre mobilné zariadenia. - Uzol infraštruktúry na rozšírenie pokrytia siete prenosom správ. Viditeľný v zozname uzlov. Smerovač s Oneskorením - Uzol infraštruktúry, ktorý vždy preposiela pakety raz, ale až po všetkých ostatných režimoch, čím zabezpečuje dodatočné pokrytie pre miestne zväzky. Viditeľný v zozname uzlov. Senzor - Prioritne vysiela telemetrické pakety. TAK - Optimalizované pre systémovú komunikáciu ATAK, znižuje rutinné vysielanie. TAK Sledovač - Umožňuje automatické vysielanie TAK PLI a znižuje rutinné vysielanie. Sledovač - Prioritne vysiela pakety polohy GPS. - Enkóder #1 aktivovaný Prijaté negatívne potvrdenie Žiadna trasa @@ -428,7 +375,6 @@ Časový limit RSSI Indikátor sily prijímaného signálu (RSSI), meranie používané na určenie úrovne výkonu prijatého skrz anténu. Vyššia hodnota RSSI vo všeobecnosti znamená silnejšie a stabilnejšie pripojenie. - Uložiť Uložiť @@ -438,7 +384,6 @@ Zabezpečenie Vybrať všetko Odoslať - Sériový nastavenia @@ -452,18 +397,13 @@ Kvalita signálu Obrazovka - Inteligentná poloha SNR Pomer signálu od šumu (SNR), miera používaná v komunikácii na kvantifikáciu úrovne požadovaného signálu k úrovni hluku pozadia. V Meshtastic a iných bezdrôtových systémoch znamená vyšší SNR jasnejší signál, ktorý môže zvýšiť spoľahlivosť a kvalitu prenosu údajov. Rýchlosť - SSID Podporované Vymazať Stlmiť - Modrá - Zelená - Červená Telemetria Téma Tmavá @@ -485,7 +425,6 @@ Trasovanie - Vysielať cez sieť LoRa LoRa MQTT @@ -502,11 +441,9 @@ Doba prevádzky URL - Užívateľ ID používateľa - Používateľské meno cez MQTT Napätie Vymazať cieľový bod? diff --git a/core/resources/src/commonMain/composeResources/values-sl/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-sl/schema_strings.xml new file mode 100644 index 0000000000..753a606bbf --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-sl/schema_strings.xml @@ -0,0 +1,43 @@ + + + + + Prekliči/zavrzi + Brez + Ime + Obravnavaj dvojni pritisk na podprtih merilnikih pospeška kot pritisk uporabnika. + Enako kot vedenje ALL, vendar preskoči dekodiranje paketkov in jih preprosto ponovno odda. Na voljo samo v vlogi Repeater. Če to nastavite za katero koli drugo vlogo, bo to povzročilo vedenje ALL. + Ignorira opažena sporočila tujih odprtih mrež, ali tistih, ki jih ne more dešifrirati. Ponovno oddaja samo sporočila na lokalnih primarnih/sekundarnih kanalih vozlišč. + Brez + Dovoljeno samo za vloge SENSOR, TRACKER in TAK_TRACKER, prepovedano bo vsakršnje ponovno oddajanje, v nasprotju z vlogo CLIENT_MUTE. + Naprava, ki oddaja samo po potrebi zaradi prikritosti ali varčevanja z energijo. + Naprava ki ne posreduje paketkov drugih naprav. + Redno oddaja lokacijo kot sporočilo privzetemu kanalu za pomoč pri vrnitvi naprave. + Prednostno oddaja paketke telemetrije. + Optimizirano za komunikacijo sistema ATAK, zmanjšuje rutinsko oddajanje. + Omogoča samodejno oddajanje TAK PLI in zmanjšuje rutinsko oddajanje. + Prednostno oddaja paketke GPS položaja. + Regija + Sporočilo + Izbira ali je treba naš NeighborInfo poleg pošiljanja v MQTT in PhoneAPI posredovati tudi prek LoRa. Ni na voljo na kanalu s privzetim ključem in imenom. + Brez + Shrani + Zasebni ključ + Javni ključ + Časovna omejitev + diff --git a/core/resources/src/commonMain/composeResources/values-sl/strings.xml b/core/resources/src/commonMain/composeResources/values-sl/strings.xml index f37a10d6e6..d6e7b2035b 100644 --- a/core/resources/src/commonMain/composeResources/values-sl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sl/strings.xml @@ -17,6 +17,8 @@ --> + Vlaga + Temperatura O programu Sprejmi @@ -27,7 +29,6 @@ Administracija Odstotek časa oddajanja v zadnji uri. - Znak opozorilnega zvonca! Aplikacija je prestara @@ -36,7 +37,6 @@ Dober Ali si prepričan spremeni na osnovno? Slab - Baterija Preračunavam… @@ -47,7 +47,6 @@ Uporaba za trenutni kanal, vključno z dobro oblikovanimi TX, RX in napačno oblikovanim RX (šum). Neveljaven kanal - Kanal Ime kanala Izberi temo @@ -57,9 +56,7 @@ Zapri - Obravnavaj dvojni pritisk na podprtih merilnikih pospeška kot pritisk uporabnika. Upravlja utripajočo LED na napravi. Pri večini naprav bo to krmililo eno od največ 4 LED diod, LED napajanja in GPS ni mogoče nadzorovati. - Izbira ali je treba naš NeighborInfo poleg pošiljanja v MQTT in PhoneAPI posredovati tudi prek LoRa. Ni na voljo na kanalu s privzetim ključem in imenom. Povezan z radiem, vendar radio "spi" Kopiraj @@ -84,17 +81,15 @@ Prekinjeno Neposreden - Razdalja + Uredi 8 Ur - Neujemanje javnega ključa Šifriranje javnega ključa - Napaka Dosežena je omejitev delovnega cikla. Trenutno ne morete pošiljati sporočil, poskusite kasneje. @@ -109,11 +104,9 @@ Dober - Skokov stran IAQ (Kakovost zraka v zaprtih prostorih) relativna vrednost IAQ na lestvici, izmerjena z Bosch BME680. Razpon vrednosti 0–500. - Prezri Dodaj '%1$s' na prezrto listo? Odstrani '%1$s' iz prezrte liste? @@ -134,7 +127,6 @@ Dnevniki - Velikost predpomnilnika: %1$d MB\nUporaba predpomnilnika: %2$d MB Upravitelj predpomnilnika Trenutna velikost predpomnilnika %1$d plošče @@ -194,6 +186,7 @@ Brez (onemogoči) Brez Ni povezano + V redu @@ -201,17 +194,15 @@ 24 Ur 1T - + - Jezik Privzeta sistemska - Zasebni ključ Zagotovi lokacijo telefona v omrežju Javni ključ QR koda @@ -223,14 +214,6 @@ Novi hitri klepet Nastavitev radia Ponovni zagon - - Ponovno oddaja vsako opaženo sporočilo, če je bilo na našem zasebnem kanalu ali iz drugega omrežja z enakimi parametri. - Enako kot vedenje ALL, vendar preskoči dekodiranje paketkov in jih preprosto ponovno odda. Na voljo samo v vlogi Repeater. Če to nastavite za katero koli drugo vlogo, bo to povzročilo vedenje ALL. - Ignorira nestandardne paketke, kot so: TAK, RangeTest, PaxCounter itd. Ponovno oddaja samo standardne paketke: NodeInfo, Text, Position, Telemetry in Routing. - Ignorira opažena sporočila iz tujih mrež, kot je LOCAL ONLY, vendar gre korak dlje, tako da ignorira tudi sporočila vozlišč, ki še niso na seznamu znanih. - Ignorira opažena sporočila tujih odprtih mrež, ali tistih, ki jih ne more dešifrirati. Ponovno oddaja samo sporočila na lokalnih primarnih/sekundarnih kanalih vozlišč. - Dovoljeno samo za vloge SENSOR, TRACKER in TAK_TRACKER, prepovedano bo vsakršnje ponovno oddajanje, v nasprotju z vlogo CLIENT_MUTE. - Regija Administracija na daljavo @@ -243,17 +226,6 @@ Ponastavi Ponastavi na osnovno - Aplikacija povezana ali samostojna naprava za sporočanje. - Naprava, ki oddaja samo po potrebi zaradi prikritosti ali varčevanja z energijo. - Naprava ki ne posreduje paketkov drugih naprav. - Infrastrukturno vozlišče za razširitev pokritosti omrežja s posredovanjem sporočil z minimalnimi stroški. Ni vidno na seznamu vozlišč. - Kombinacija ROUTER in CLIENT. Ni za mobilne naprave. - Infrastrukturno vozlišče za razširitev pokritosti omrežja s posredovanjem sporočil. Vidno na seznamu vozlišč. - Infrastrukturno vozlišče, ki vedno znova oddaja paketke enkrat, vendar šele po vseh drugih načinih, kar zagotavlja dodatno pokritost za lokalne gruče. Vidno na seznamu vozlišč. - Prednostno oddaja paketke telemetrije. - Optimizirano za komunikacijo sistema ATAK, zmanjšuje rutinsko oddajanje. - Omogoča samodejno oddajanje TAK PLI in zmanjšuje rutinsko oddajanje. - Prednostno oddaja paketke GPS položaja. Prejeta negativna potrditev Brez poti @@ -261,7 +233,6 @@ Časovna omejitev RSSI Indikator moči sprejetega signala je meritev, ki se uporablja za določanje ravni moči, ki jo sprejema antena. Višja vrednost RSSI na splošno pomeni močnejšo in stabilnejšo povezavo. - Shrani Shrani @@ -269,7 +240,6 @@ Izberi vse Pošlji - Souporaba Delite z… @@ -311,7 +281,6 @@ Neznano uporabniško ime Neprepoznano - Preko MQTT Izbriši točko poti? diff --git a/core/resources/src/commonMain/composeResources/values-sq/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-sq/schema_strings.xml new file mode 100644 index 0000000000..0c06ac536e --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-sq/schema_strings.xml @@ -0,0 +1,39 @@ + + + + + Anullo + Asnjë + Emri + Po të njëjtën sjellje si ALL, por kalon pa dekoduar paketat dhe thjesht i ritransmeton. I disponueshëm vetëm për rolin Repeater. Vendosja e kësaj në rolet e tjera do të rezultojë në sjelljen e ALL. + Injoron mesazhet e vëzhguara nga rrjete të huaja që janë të hapura ose ato që nuk mund t'i dekodoj. Vetëm ritransmeton mesazhe në kanalet lokale primare / dytësore të nyjës. + Asnjë + Lejohet vetëm për rolet SENSOR, TRACKER dhe TAK_TRACKER, kjo do të pengojë të gjitha ritransmetimet, jo ndryshe nga roli CLIENT_MUTE. + Pajisje që transmeton vetëm kur është e nevojshme për fshehtësi ose kursim energjie. + Pajisje që nuk kalon paketa nga pajisje të tjera. + Transmeton vendndodhjen si mesazh në kanalin e parazgjedhur rregullisht për të ndihmuar në rikuperimin e pajisjeve. + Transmeton paketa telemetri si prioritet. + Optimizuar për komunikim në sistemin ATAK, zvogëlon transmetimet rutinë. + Aktivizon transmetimet automatikisht TAK PLI dhe zvogëlon transmetimet rutinë. + Transmeton paketa pozicioni GPS si prioritet. + Rajon + Mesazh + Asnjë + Ruaj + Koha e skaduar + diff --git a/core/resources/src/commonMain/composeResources/values-sq/strings.xml b/core/resources/src/commonMain/composeResources/values-sq/strings.xml index 4d301cebdd..84bcdfe23b 100644 --- a/core/resources/src/commonMain/composeResources/values-sq/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sq/strings.xml @@ -17,6 +17,8 @@ --> + Lagështia + Temperatura Rreth Prano @@ -27,7 +29,6 @@ Administratë Përqindja e kohës së përdorur për transmetim brenda orës së kaluar. - Përditësimi i aplikacionit kërkohet Apliko @@ -35,7 +36,6 @@ Mirë A jeni të sigurt se doni të kaloni në kanalin e parazgjedhur? I Keq - Bateria Po llogaritet… @@ -46,7 +46,6 @@ Përdorimi për kanalin aktual, duke përfshirë TX të formuar mirë, RX dhe RX të dëmtuar (në gjuhën e thjeshtë: zhurmë). Ky URL kanal është i pavlefshëm dhe nuk mund të përdoret - Kanal Emri i kanalit radio Zgjidh temën @@ -82,17 +81,15 @@ Direkt 訊息 - Distanca + Redakto 8 Orë - Përputhje e Gabuar e Çelësit Publik Kriptimi me Çelës Publik - Gabim Cikli i detyrës ka arritur kufirin. Nuk mund të dërgoni mesazhe tani, ju lutem provoni përsëri më vonë. @@ -107,10 +104,8 @@ Mirë - Hops larg (Cilësia e Ajrit të Brendshëm) shkalla relative e vlerës IAQ siç matet nga Bosch BME680. Intervali i Vlerave 0–500. - Injoro Të shtohet ‘%1$s’ në listën e injoruar? Të hiqet ‘%1$s’ nga lista e injoruar? @@ -131,7 +126,6 @@ Loget - Kapasiteti i Cache: %1$d MB\nPërdorimi i Cache: %2$d MB Menaxheri i Cache Madhësia e aktuale e cache %1$d pllaka @@ -191,19 +185,19 @@ Asnjë (çaktivizo) Asnjë Nuk është lidhur + Mirë 24 Orë - + - Gjuhë Parazgjedhje sistemi @@ -217,14 +211,6 @@ Bisedë e re e shpejtë Konfigurimi i radios Rindiz - - Ritransmeton çdo mesazh të vërejtur, nëse ishte në kanalin tonë privat ose nga një tjetër rrjet me të njëjtat parametra LoRa. - Po të njëjtën sjellje si ALL, por kalon pa dekoduar paketat dhe thjesht i ritransmeton. I disponueshëm vetëm për rolin Repeater. Vendosja e kësaj në rolet e tjera do të rezultojë në sjelljen e ALL. - Injoron paketat nga portnumra jo standardë si: TAK, RangeTest, PaxCounter, etj. Vetëm ritransmeton paketat me portnumra standard: NodeInfo, Text, Position, Telemetry, dhe Routing. - Injoron mesazhet e vëzhguara nga rrjete të huaja si LOCAL ONLY, por e çon më tutje duke injoruar edhe mesazhet nga nyje që nuk janë në listën e njohur të nyjës. - Injoron mesazhet e vëzhguara nga rrjete të huaja që janë të hapura ose ato që nuk mund t'i dekodoj. Vetëm ritransmeton mesazhe në kanalet lokale primare / dytësore të nyjës. - Lejohet vetëm për rolet SENSOR, TRACKER dhe TAK_TRACKER, kjo do të pengojë të gjitha ritransmetimet, jo ndryshe nga roli CLIENT_MUTE. - Rajon Administratë e Largët @@ -237,23 +223,12 @@ Rivendos Rivendos në parazgjedhje - Pajisje e lidhur ose pajisje mesazhi autonome. - Pajisje që transmeton vetëm kur është e nevojshme për fshehtësi ose kursim energjie. - Pajisje që nuk kalon paketa nga pajisje të tjera. - Nyjë infrastrukture për zgjerimin e mbulimit të rrjetit duke transmetuar mesazhe me ngarkesë minimale. Nuk është e dukshme në listën e nyjeve. - Kombinim i të dyjave ROUTER dhe CLIENT. Nuk është për pajisje mobile. - Nyjë infrastrukture për zgjerimin e mbulimit të rrjetit duke transmetuar mesazhe. E dukshme në listën e nyjeve. - Transmeton paketa telemetri si prioritet. - Optimizuar për komunikim në sistemin ATAK, zvogëlon transmetimet rutinë. - Aktivizon transmetimet automatikisht TAK PLI dhe zvogëlon transmetimet rutinë. - Transmeton paketa pozicioni GPS si prioritet. Marrë një njohje negative Nuk ka rrugë Pranuar Koha e skaduar Indikatori i Fuqisë së Sinjalit të Marrë, një matje e përdorur për të përcaktuar nivelin e energjisë që po merret nga antena. Një vlerë më e lartë RSSI zakonisht tregon një lidhje më të fortë dhe më të qëndrueshme. - Ruaj Ruaj @@ -261,7 +236,6 @@ Përzgjedh të gjithë Dërgo - Ndaj @@ -291,7 +265,6 @@ Emri i përdoruesit është i panjohur I panjohur - përmes MQTT Të fshihet pika e rreshtit? diff --git a/core/resources/src/commonMain/composeResources/values-sr/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-sr/schema_strings.xml new file mode 100644 index 0000000000..9a9b07ede6 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-sr/schema_strings.xml @@ -0,0 +1,251 @@ + + + + + Струја + LED статус + Стање LED диоде (укључено/искључено) + Подразумевано + Фиксни ПИН + Мод упаривања + Фиксни ПИН + Нема ПИН-а (само ради) + Ротациони догађај у смеру супротном од казаљке на сату + Ротациони догађај у смеру казаљке на сату + Догађај притиска енкодера + GPIO пин за A порт ротационог енкодера + GPIO пин за A порт ротационог енкодера. + GPIO пин за Б порт ротационог енкодера + GPIO пин за Б порт ротационог енкодера. + GPIO пин за порт клика ротационог енкодера + GPIO пин за порт клика ротационог енкодера. + Назад + Otkaži + Доле + Лево + Bez + Десно + Изабери + Горе + Ротациони 1 + Пошаљи звоно + Горе Доле 1 + Тип покретача + Омогућава модул сензора детекције. Потребно је да буде омогућен и на чвору са сензором, и на свим чворовима које желите да примате текстуалне поруке сензора детекције или да видите дневник и графикон сензора детекције. + Минимално време између емитовања детекције + GPIO пин за надгледање + Пријатељски назив + Пошаљи звоно + Пошаљи ASCII звона са поруком упозорења. Корисно за покретање спољашњег обавештења на звону. + Интервал емитовања стања + Користи pull-up отпорник + Да ли се користи режим INPUT_PULLUP за GPIO пин. Примењује се само ако плоча користи pull-up отпорнике на пину. + Дугме GPIO + Звучни сигнал GPIO + Двоструки додир као дугме + Treniraj dvostruki dodir na podržanim akcelerometrima kao pritisak korisničkog dugmeta. + LED срчани откуцаји + Контролише трептајући ЛЕД на уређају. За већину уређаја ово ће контролисати један од до максималних 4 ЛЕД, ЛЕД пуњења и ГПС ЛЕД диоде се не могу контролисати. + Интервал емитовања информација о чвору + Режим реемитовања + Сви + Isto kao ponašanje kod ALL moda, ali preskače dekodiranje paketa i jednostavno ih ponovo prenosi. Dostupno samo u Repeater ulozi. Postavljanje ovoga na bilo koju drugu ulogu rezultovaće ALL ponašanjem. + Ignoriše primećene poruke iz stranih mreža koje su otvorene ili one koje ne može da dekodira. Ponovo prenosi poruku samo na lokalne primarne/sekundarne kanale čvora. + Bez + Dozvoljeno samo za uloge SENSOR, TRACKER, TAK_TRACKER, ovo će onemogućiti sve ponovne prenose, slično kao uloga CLIENT_MUTE. + Улога уређаја + Клијент + Апликација повезана или самостални уређај за размену порука. + Скривени клијент + Uređaj koji prenosi samo kada je potrebno radi skrivenosti ili uštede energije. + Клијент мутиран + Uređaj koji ne prosleđuje pakete od drugih uređaja. + Изгубљено и нађено + Prenosi lokaciju kao poruku na podrazumevani kanal redovno kako bi pomogao u pronalasku uređaja. + Поновљач + Рутер + Инфраструктурни чвор само на торњу или врху планине. Не користи се за кровове или мобилне чворове. Потребна му је изузетна покривеност. Видљиво на листи чворова. + Рутер са кашњењем + Сензор + Emituje telemetrijske pakete kao prioritet. + Optimizovano za komunikaciju u ATAK sistemu, smanjuje rutinske emisije. + ТАК Трекер + Omogućava autmatske TAK PLI emisije i smanjuje rutinske emisije. + Трекер + Emituje GPS pakete položaja kao prioritet. + Временска зона + Интервал карусела + Аутоматски се пребацује на следећу страницу на екрану као карусел, на основу наведеног интервала. + Увек усмеравајте на север + Смер компаса на екрану изван круга увек ће указивати на север. + Режим приказа + TFT екрани у пуној боји + Подразумевани изглед екрана 128x64 + Обрнута горња трака за екран у 2 боје + Оптимизовано за двобојне дисплеје + Метрика + Окрени екран + Окрени екран вертикално. + Тип OLED-а + Премаши аутоматско откривање OLED екрана. + Екран укључен за + Колико дуго екран остаје укључен након притиска корисничког дугмета или пријема порука. + Јединице приказа + Јединице приказане на екрану уређаја. + Пробуди екран додиром или покретом + Захтева да уређај има акцелерометар. + Активан + Ако је омогућено, 'output' пин ће бити активиран на високом нивоу, а ако је онемогућено, биће активиран на ниском нивоу. + Упозори када примиш звоно + Упозорите GPIO зујалицу када примите звоно + Упозорите GPIO вибра мотор када примите звоно + Упозори када примиш поруку + Упозорите GPIO зујалицу када примите поруку + Упозорите GPIO вибра мотор када примите поруку + Излазни пин GPIO + Трајање GPIO излаза + Излазни пин за вибрацију GPIO + Користи I2S као звучник + Омогућава уређајима са изворним I2S аудио излазом да користе РТТТЛ преко звучника као звучник. Т-Ватцх СКСНУМКС и Т-Децк на пример имају ову могућност. + Користи PWM звучник + Користи PWM излаз (као што је RAK звучник) за мелодије уместо укључивања/искључивања излаза. Ово ће игнорисати подешавања излаза, трајање излаза и активна подешавања и користити подешавање GPIO опције звучника у конфигурацији уређаја. + Проток + Фреквенцијски слот + Оперативна фреквенција вашег нода се израчунава на основу региона, модемског пресета и овог поља. Када је 0, слот се аутоматски израчунава на основу назива примарног канала. + Стопа кодирања + Позитиван за MQTT + Подешава максималан број скокова. Подразумевано је 3, а повећање броја одобрених скокова такође повећава загушење и треба га користити опрезно. Поруке емитоване са 0 скокова неће добити потврде пријема (ACK). + Игнориши MQTT + Унапред подешено + Дугачки домет - Брзо + Дугачки домет - Умерено + Дугачки домет - Споро + Средњи домет - Брзо + Средњи домет - Споро + Кратки домет - Брзо + Кратки домет - Споро + Кратки домет - Турбо + Измена фреквенције + Регион + Регион у коме ћете користити ваше радио уређаје. + Аустралија / Нови Зеланд + Кина + Европска унија 433MHz + Европска унија 868MHz + Индија + Јапан + Кореја + 2.4 GHz + Малезија 433MHz + Малезија 919MHz + Нови зеланд 865MHz + Филипини 433MHz + Филипини 868MHz + Филипини 915MHz + Русија + Сингапур 923MHz + Тајланд + Тајван + Украјина 433MHz + Украјина 868MHz + Молимо изаберите регион + Сједињене Америчке државе + Фактор ширења + Појачање пријемника + Трансмитер укључен + Користи предефинисано подешавање + Интервал објављивања мапе + Порука + Адреса + Омогућено шифровање + Лозинка + MQTT посредник клијента + Користи мрежну везу на вашем телефону за повезивање са MQTT. + Корен тема + TLS укључен + Корисничко име + Da li bi osim slanja na MQTT i PhoneAPI, naš NeighborInfo trebalo da se prenosi preko LoRa. Nije dostupno na kanalu sa podrazumevanim ključem i imenom. + Интервал ажурирања + Омогућавање етернета ће онемогућити блутут везу са апликацијом. + Bez + Омогућавање ВајФаја ће онемогућити блутут везу са апликацијом. + Лозинка + Када је омогућен, модул бројача пролазника броји број људи који пролазе користећи ВајФај и Блутут. И ВајФај и Блутут морају бити онемогућени да би бројач пролазника радио. + Интервал ажурирања + Колико често можемо послати поруку мрежи када се открију људи. + Минимум раздаљине + Минимална промена растојања у метрима која ће се узети у обзир за паметно емитовање позиције. + Минимални интервал + Фиксна локација + Интервал ажурирања + Онемогућено + Омогућено + Није пристуно + Интервал емитовања + Паметно позиционирање + Заставице позиције + Опциони поља за укључивање при склапању порука о позицији. Што више поља је укључено, порука ће бити већа, што доводи до дужег времена емитовања и већег ризика од губитка пакета. + Висина + Надморска висина је средњи ниво мора + Висинска геоидна сепарација + Правац возила + Број сателита + Број секвенце + Брзина возила + Временска ознака + GPS пријем GPIO + GPS предаја GPIO + Преписивање ADC-а + Уштеда енергије + Спаваће све што је више могуће, за улогу трагача и сензора ово ће укључивати и лора радио. Не користите ово подешавање ако желите да користите свој уређај са мобилним апликацијама или користите уређај без корисничког дугмета. + Искључи уређај при губитку напајања + Сачувај + Снима CSV са детаљима порука теста домета, тренутно доступно само на ESP32 уређајима са веб сервером. + Инерварл пошиљаоца + Овај уређај ће слати поруке за тестирање домета у одабраном интервалу. + Дебаг логови + Излаз дебаговања уживо преко серијског интерфејса, прегледајте и извозите логове уређаја са редукованим позицијама преко блутута. + Управљани уређај + Уређајем управља администратор мреже, корисник не може да приступи ниједном подешавању уређаја. + Privatni ključ + Javni ključ + Серијска конзола + Серијска конзола преко Stream API-ја. + Ехо + Ако је подешено, сви пакети које пошаљете ће бити враћени (ехо) назад на ваш уређај. + Мод + Пријемни податак (rxd) GPIO пин + Подразумевано + Подразумевано + NMEA позиције + Протобафови + Једноставни + Текстуална порука + Isteklo vreme + Време чекања пре него што сматрамо да је ваш пакет завршен. + GPIO pin за трансмисију података (txd) + Пошаљи откуцај срца + Максимални повратак историје + Временски прозор поврата историје + Сервер + Број записа + Улога + Приказ фаренхајта + Прикажи на екрану уређаја + Снага екрана + diff --git a/core/resources/src/commonMain/composeResources/values-sr/strings.xml b/core/resources/src/commonMain/composeResources/values-sr/strings.xml index ff08e608cc..a5190fced5 100644 --- a/core/resources/src/commonMain/composeResources/values-sr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sr/strings.xml @@ -17,6 +17,8 @@ --> + Влажност + Температура O nama Prihvati @@ -33,12 +35,10 @@ Напредно Проценат искоришћења ефирског времена за пренос у последњем сату. - Karakter zvona za upozorenja! Сви Висина Висина - Увек усмеравајте на север Амбијентално осветљење Подешавања амбијенталног осветљења @@ -55,17 +55,12 @@ Да ли сте сигурни да желите да промените на подразумевани канал? Назад Loš - - Проток Батерија Блутут Блутут подешавања Блутут Подешавања - Интервал емитовања - Дугме GPIO - Звучни сигнал GPIO Прорачунавање… Дозволе за употребу камере Otkaži @@ -82,7 +77,6 @@ Канал 7 Канал 8 Ovaj URL kanala je nevažeći i ne može se koristiti. - Kanal Naziv kanala Линк канала Канали @@ -92,31 +86,14 @@ Očisti Затвори - Стопа кодирања - Treniraj dvostruki dodir na podržanim akcelerometrima kao pritisak korisničkog dugmeta. Kontroliše trepćući LED na uređaju. Kod većine uređaja ovo će kontrolisati jedan od do četiri LED-a, punjač i GPS LED-ovi nisu kontrolisani. - Da li bi osim slanja na MQTT i PhoneAPI, naš NeighborInfo trebalo da se prenosi preko LoRa. Nije dostupno na kanalu sa podrazumevanim ključem i imenom. Пошаљи позицију на примарном каналу када се корисничко дугме три пута кликне. Временска зона за датуме на екрану уређаја и у евиденцији. - Аутоматски се пребацује на следећу страницу на екрану као карусел, на основу наведеног интервала. - Смер компаса на екрану изван круга увек ће указивати на север. - Окрени екран вертикално. - Премаши аутоматско откривање OLED екрана. - Колико дуго екран остаје укључен након притиска корисничког дугмета или пријема порука. - Јединице приказане на екрану уређаја. - Захтева да уређај има акцелерометар. - Подешава максималан број скокова. Подразумевано је 3, а повећање броја одобрених скокова такође повећава загушење и треба га користити опрезно. Поруке емитоване са 0 скокова неће добити потврде пријема (ACK). Доступна унапред подешена подешавања модема, подразумевана је Long Fast. Регион у коме ћете користити ваше радио уређаје. - Минимална промена растојања у метрима која ће се узети у обзир за паметно емитовање позиције. - Опциони поља за укључивање при склапању порука о позицији. Што више поља је укључено, порука ће бити већа, што доводи до дужег времена емитовања и већег ризика од губитка пакета. - Спаваће све што је више могуће, за улогу трагача и сензора ово ће укључивати и лора радио. Не користите ово подешавање ако желите да користите свој уређај са мобилним апликацијама или користите уређај без корисничког дугмета. - Излаз дебаговања уживо преко серијског интерфејса, прегледајте и извозите логове уређаја са редукованим позицијама преко блутута. - Уређајем управља администратор мреже, корисник не може да приступи ниједном подешавању уређаја. Користи се за креирање заједничког кључа са удаљеним уређајем. - Серијска конзола преко Stream API-ја. Блутут повезан Povezan na radio uređaj, ali uređaj je u stanju spavanja Стандардно @@ -153,13 +130,13 @@ Uređaj je u stanju spavanja Директне поруке Директне поруке + Онемогућено Прекините везу Raskačeno Датум Direktno Поруке - Приказ Udaljenost @@ -168,16 +145,14 @@ Ажурирања фирмвера Чворови Документација - Двоструки додир као дугме + Измени 8 Сати Омогућено - Неусаглашеност јавних кључева Шифровање јавним кључем Метрике сензора - Окружење Грешка Достигнут је лимит циклуса рада. Не могу слати поруке тренутно, молимо вас покушајте касније. @@ -203,18 +178,10 @@ Белешке о издању Фиксна локација - Фреквенцијски слот - Пријатељски назив Генерисање QR кода Искључен Dobro - - GPIO пин за A порт ротационог енкодера - GPIO пин за Б порт ротационог енкодера - GPIO пин за порт клика ротационог енкодера - GPS пријем GPIO - GPS предаја GPIO Хардвер Смер Скокова удаљено @@ -222,10 +189,8 @@ Знам шта радим. IAQ (Kvalitet vazduha u zatvorenom prostoru) relativna skala vrednosti IAQ merena Bosch BME680. Raspon vrednosti 0–500. - Игнориши Додати '%1$s' на листу игнорисаних? - Игнориши MQTT Уклнити '%1$s' на листу игнорисаних? Квалитет ваздуха у затвореном простору (IAQ) @@ -237,7 +202,6 @@ Ширина - LED срчани откуцаји @@ -255,7 +219,6 @@ Низак ниво батерије: %1$s Мапа меша - Капацитет кеш меморије: %1$d MB\n Употреба кеш меморије: %2$d MB Меначер кеш меморије Тренутна величина кеш меморије %1$d плочице @@ -329,7 +292,6 @@ Poslednji put viđeno preko MQTT-a Ресетовање базе чворова - Интервал емитовања информација о чвору Чворови Нема повезаних уређаја @@ -338,29 +300,25 @@ Bez Nije povezan Белешке + - Број записа - Позитиван за MQTT Океј 24 Сати 1n Опције - - Измена фреквенције Лозинка + Позиција - Заставице позиције Позиција Пакети позиција - Снага Подешавања напајња Мерни подаци о снази @@ -369,7 +327,6 @@ Подразумевано системско подешавање Примарни - Privatni ključ Информације о пројекту Обезбедите локацију телефона меш мрежи Javni ključ @@ -384,15 +341,6 @@ Тест домета Конфигурација теста домета Поново покрени - - Режим реемитовања - Ponovo prenosi svaku primećenu poruku, ako je bila na našem privatnom kanali ili iz druge mreže sa istim LoRA parametrima. - Isto kao ponašanje kod ALL moda, ali preskače dekodiranje paketa i jednostavno ih ponovo prenosi. Dostupno samo u Repeater ulozi. Postavljanje ovoga na bilo koju drugu ulogu rezultovaće ALL ponašanjem. - Ignoriše pakete sa nestandardnim brojevima porta kao što su: TAK, RangeTest, PaxCounter, itd. Ponovo prenosi samo pakete sa standardnim brojevima porta: NodeInfo, Text, Position, Temeletry i Routing. - Ignoriše primećene poruke iz stranih mreža kao LOCAL ONLY, ali ide korak dalje tako što takođe ignoriše poruke sa čvorova koji nisu već na listi nepoznatih čvorova. - Ignoriše primećene poruke iz stranih mreža koje su otvorene ili one koje ne može da dekodira. Ponovo prenosi poruku samo na lokalne primarne/sekundarne kanale čvora. - Dozvoljeno samo za uloge SENSOR, TRACKER, TAK_TRACKER, ovo će onemogućiti sve ponovne prenose, slično kao uloga CLIENT_MUTE. - Регион Udaljena administracija @@ -414,26 +362,15 @@ Улога уређаја Клијент - Povezana aplikacija ili samostalni uređaj za slanje poruka. Скривени клијент - Uređaj koji prenosi samo kada je potrebno radi skrivenosti ili uštede energije. Клијент мутиран - Uređaj koji ne prosleđuje pakete od drugih uređaja. Изгубљено и нађено Поновљач - Infrastrukturni čvor za proširenje pokrivenosti mreže prosleđivanjem poruka sa minimalnim troškovima energije. Nije vidljiv na listi čvorova. Рутер - Kombinacija i RUTERA i KLIJENTA. Nije namenjeno za mobilne uređaje. - Infrastrukturni čvor za proširenje pokrivenosti mreže prosleđivanjem poruka. Vidljiv na listi čvorova. Рутер са кашњењем - Infrastrukturni čvor koji uvek ponovo prenosi pakete jednom, ali tek nakon svih drugih načina, osiguravajući dodatno pokrivanje za lokalne klastere. Vidljiv u listi čvorova. Сензор - Emituje telemetrijske pakete kao prioritet. - Optimizovano za komunikaciju u ATAK sistemu, smanjuje rutinske emisije. ТАК Трекер - Omogućava autmatske TAK PLI emisije i smanjuje rutinske emisije. Трекер - Emituje GPS pakete položaja kao prioritet. Primljena negativna potvrda Nema rute @@ -442,12 +379,10 @@ RSSI Indikator jačine primljenog signala RSSI, merenje koje se koristi za određivanje nivoa snage koji antena prima. Viša vrednost RSSI generalno ukazuje na jaču i stabilniju vezu. Сателита - Сачувај Сачувај Скенирај - Екран укључен за Секундарни Сигурност @@ -455,10 +390,8 @@ Изабери Изабери све Pošalji - Серијска веза Подешавања серијске везе - Сервер подешавања Podeli @@ -470,15 +403,12 @@ Kvalitet signala Приказ - Паметно позиционирање SNR Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података. Брзина - Фактор ширења Подржан Обриши Утишај - Појачање пријемника Сервер Искључен @@ -490,7 +420,6 @@ Светла Прати систем Време - Временска зона Isteklo vreme Временска ознака Праћење руте @@ -514,7 +443,6 @@ 28č 48 Сати 2n - Трансмитер укључен Прати систем Метрика @@ -525,12 +453,9 @@ Nekategorisano Време рада - - Користи предефинисано подешавање Корисник Корисничка подешавања - Корисничко име preko MQTT-a Напон Обрисати тачку путање? diff --git a/core/resources/src/commonMain/composeResources/values-srp/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-srp/schema_strings.xml new file mode 100644 index 0000000000..1383b13700 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-srp/schema_strings.xml @@ -0,0 +1,251 @@ + + + + + Струја + LED статус + Стање LED диоде (укључено/искључено) + Подразумевано + Фиксни ПИН + Мод упаривања + Фиксни ПИН + Нема ПИН-а (само ради) + Ротациони догађај у смеру супротном од казаљке на сату + Ротациони догађај у смеру казаљке на сату + Догађај притиска енкодера + GPIO пин за A порт ротационог енкодера + GPIO пин за A порт ротационог енкодера. + GPIO пин за Б порт ротационог енкодера + GPIO пин за Б порт ротационог енкодера. + GPIO пин за порт клика ротационог енкодера + GPIO пин за порт клика ротационог енкодера. + Назад + Откажи + Доле + Лево + Ништа + Десно + Изабери + Горе + Ротациони 1 + Пошаљи звоно + Горе Доле 1 + Тип покретача + Омогућава модул сензора детекције. Потребно је да буде омогућен и на чвору са сензором, и на свим чворовима које желите да примате текстуалне поруке сензора детекције или да видите дневник и графикон сензора детекције. + Минимално време између емитовања детекције + GPIO пин за надгледање + Пријатељски назив + Пошаљи звоно + Пошаљи ASCII звона са поруком упозорења. Корисно за покретање спољашњег обавештења на звону. + Интервал емитовања стања + Користи pull-up отпорник + Да ли се користи режим INPUT_PULLUP за GPIO пин. Примењује се само ако плоча користи pull-up отпорнике на пину. + Дугме GPIO + Звучни сигнал GPIO + Двоструки додир као дугме + Третирај двоструки тап на подржаним акцелерометрима као притисак корисничког дугмета. + LED срчани откуцаји + Контролише трептајући ЛЕД на уређају. За већину уређаја ово ће контролисати један од до максималних 4 ЛЕД, ЛЕД пуњења и ГПС ЛЕД диоде се не могу контролисати. + Интервал емитовања информација о чвору + Режим реемитовања + Сви + Исто као понашање као ALL, али прескаче декодирање пакета и једноставно их поново преноси. Доступно само у Repeater улози. Постављање овога на било коју другу улогу резултираће ALL понашањем. + Игнорише примећене поруке из страних мрежа које су отворене или оне које не може да декодира. Поново преноси поруку само на локалне примарне/секундарне канале чвора. + Ништа + Дозвољено само за улоге SENSOR, TRACKER и TAK_TRACKER, ово ће онемогућити све поновне преносе, слично као улога CLIENT_MUTE. + Улога уређаја + Клијент + Апликација повезана или самостални уређај за размену порука. + Скривени клијент + Уређај који емитује само по потреби ради прикривености или уштеде енергије. + Клијент мутиран + Уређај који не прослеђује пакете примљене од других уређаја. + Изгубљено и нађено + Редовно емитује локацију као поруку подразумеваном каналу ради помоћи при проналаску уређаја. + Поновљач + Рутер + Инфраструктурни чвор само на торњу или врху планине. Не користи се за кровове или мобилне чворове. Потребна му је изузетна покривеност. Видљиво на листи чворова. + Рутер са кашњењем + Сензор + Емитује телеметријске пакете као приоритет. + Оптимизован за комуникацију са ATAK системом, смањује рутинске емисије. + ТАК Трекер + Омогућава аутоматске TAK PLI емисије и смањује рутинске емисије. + Трекер + Емитује пакете са GPS позицијом као приоритет. + Временска зона + Интервал карусела + Аутоматски се пребацује на следећу страницу на екрану као карусел, на основу наведеног интервала. + Увек усмеравајте на север + Смер компаса на екрану изван круга увек ће указивати на север. + Режим приказа + TFT екрани у пуној боји + Подразумевани изглед екрана 128x64 + Обрнута горња трака за екран у 2 боје + Оптимизовано за двобојне дисплеје + Метрика + Окрени екран + Окрени екран вертикално. + Тип OLED-а + Премаши аутоматско откривање OLED екрана. + Екран укључен за + Колико дуго екран остаје укључен након притиска корисничког дугмета или пријема порука. + Јединице приказа + Јединице приказане на екрану уређаја. + Пробуди екран додиром или покретом + Захтева да уређај има акцелерометар. + Активан + Ако је омогућено, 'output' пин ће бити активиран на високом нивоу, а ако је онемогућено, биће активиран на ниском нивоу. + Упозори када примиш звоно + Упозорите GPIO зујалицу када примите звоно + Упозорите GPIO вибра мотор када примите звоно + Упозори када примиш поруку + Упозорите GPIO зујалицу када примите поруку + Упозорите GPIO вибра мотор када примите поруку + Излазни пин GPIO + Трајање GPIO излаза + Излазни пин за вибрацију GPIO + Користи I2S као звучник + Омогућава уређајима са изворним I2S аудио излазом да користе РТТТЛ преко звучника као звучник. Т-Ватцх СКСНУМКС и Т-Децк на пример имају ову могућност. + Користи PWM звучник + Користи PWM излаз (као што је RAK звучник) за мелодије уместо укључивања/искључивања излаза. Ово ће игнорисати подешавања излаза, трајање излаза и активна подешавања и користити подешавање GPIO опције звучника у конфигурацији уређаја. + Проток + Фреквенцијски слот + Оперативна фреквенција вашег нода се израчунава на основу региона, модемског пресета и овог поља. Када је 0, слот се аутоматски израчунава на основу назива примарног канала. + Стопа кодирања + Позитиван за MQTT + Подешава максималан број скокова. Подразумевано је 3, а повећање броја одобрених скокова такође повећава загушење и треба га користити опрезно. Поруке емитоване са 0 скокова неће добити потврде пријема (ACK). + Игнориши MQTT + Унапред подешено + Дугачки домет - Брзо + Дугачки домет - Умерено + Дугачки домет - Споро + Средњи домет - Брзо + Средњи домет - Споро + Кратки домет - Брзо + Кратки домет - Споро + Кратки домет - Турбо + Измена фреквенције + Регион + Регион у коме ћете користити ваше радио уређаје. + Аустралија / Нови Зеланд + Кина + Европска унија 433MHz + Европска унија 868MHz + Индија + Јапан + Кореја + 2.4 GHz + Малезија 433MHz + Малезија 919MHz + Нови зеланд 865MHz + Филипини 433MHz + Филипини 868MHz + Филипини 915MHz + Русија + Сингапур 923MHz + Тајланд + Тајван + Украјина 433MHz + Украјина 868MHz + Молимо изаберите регион + Сједињене Америчке државе + Фактор ширења + Појачање пријемника + Трансмитер укључен + Користи предефинисано подешавање + Интервал објављивања мапе + Порука + Адреса + Омогућено шифровање + Лозинка + MQTT посредник клијента + Користи мрежну везу на вашем телефону за повезивање са MQTT. + Корен тема + TLS укључен + Корисничко име + Да ли би поред слања на MQTT и PhoneAPI, наша NeighborInfo требало да се преноси преко LoRa? Није доступно на каналу са подразумеваним кључем и именом. + Интервал ажурирања + Омогућавање етернета ће онемогућити блутут везу са апликацијом. + Ништа + Омогућавање ВајФаја ће онемогућити блутут везу са апликацијом. + Лозинка + Када је омогућен, модул бројача пролазника броји број људи који пролазе користећи ВајФај и Блутут. И ВајФај и Блутут морају бити онемогућени да би бројач пролазника радио. + Интервал ажурирања + Колико често можемо послати поруку мрежи када се открију људи. + Минимум раздаљине + Минимална промена растојања у метрима која ће се узети у обзир за паметно емитовање позиције. + Минимални интервал + Фиксна локација + Интервал ажурирања + Онемогућено + Омогућено + Није пристуно + Интервал емитовања + Паметно позиционирање + Заставице позиције + Опциони поља за укључивање при склапању порука о позицији. Што више поља је укључено, порука ће бити већа, што доводи до дужег времена емитовања и већег ризика од губитка пакета. + Висина + Надморска висина је средњи ниво мора + Висинска геоидна сепарација + Правац возила + Број сателита + Број секвенце + Брзина возила + Временска ознака + GPS пријем GPIO + GPS предаја GPIO + Преписивање ADC-а + Уштеда енергије + Спаваће све што је више могуће, за улогу трагача и сензора ово ће укључивати и лора радио. Не користите ово подешавање ако желите да користите свој уређај са мобилним апликацијама или користите уређај без корисничког дугмета. + Искључи уређај при губитку напајања + Сачувај + Снима CSV са детаљима порука теста домета, тренутно доступно само на ESP32 уређајима са веб сервером. + Инерварл пошиљаоца + Овај уређај ће слати поруке за тестирање домета у одабраном интервалу. + Дебаг логови + Излаз дебаговања уживо преко серијског интерфејса, прегледајте и извозите логове уређаја са редукованим позицијама преко блутута. + Управљани уређај + Уређајем управља администратор мреже, корисник не може да приступи ниједном подешавању уређаја. + Приватни кључ + Јавни кључ + Серијска конзола + Серијска конзола преко Stream API-ја. + Ехо + Ако је подешено, сви пакети које пошаљете ће бити враћени (ехо) назад на ваш уређај. + Мод + Пријемни податак (rxd) GPIO пин + Подразумевано + Подразумевано + NMEA позиције + Протобафови + Једноставни + Текстуална порука + Временско ограничење + Време чекања пре него што сматрамо да је ваш пакет завршен. + GPIO pin за трансмисију података (txd) + Пошаљи откуцај срца + Максимални повратак историје + Временски прозор поврата историје + Сервер + Број записа + Улога + Приказ фаренхајта + Прикажи на екрану уређаја + Снага екрана + diff --git a/core/resources/src/commonMain/composeResources/values-srp/strings.xml b/core/resources/src/commonMain/composeResources/values-srp/strings.xml index aedf3abe15..118b6189ad 100644 --- a/core/resources/src/commonMain/composeResources/values-srp/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-srp/strings.xml @@ -17,6 +17,8 @@ --> + Влажност + Температура О Прихвати @@ -33,12 +35,10 @@ Напредно Проценат искоришћења ефирског времена за пренос у последњем сату. - Карактер звона за упозорења! Сви Висина Висина - Увек усмеравајте на север Амбијентално осветљење Подешавања амбијенталног осветљења @@ -55,17 +55,12 @@ Да ли сте сигурни да желите да промените на подразумевани канал? Назад Лош - - Проток Батерија Блутут Блутут подешавања Блутут Подешавања - Интервал емитовања - Дугме GPIO - Звучни сигнал GPIO Прорачунавање… Дозволе за употребу камере Откажи @@ -82,7 +77,6 @@ Канал 7 Канал 8 Ова URL адреса канала је неважећа и не може се користити - Канал Назив канала Линк канала Канали @@ -92,31 +86,14 @@ Очисти Затвори - Стопа кодирања - Третирај двоструки тап на подржаним акцелерометрима као притисак корисничког дугмета. Контролише трепћућу LED лампицу на уређају. За већину уређаја ово ће контролисати једну од до 4 LED лампице, LED лампице за пуњач и GPS нису контролисане. - Да ли би поред слања на MQTT и PhoneAPI, наша NeighborInfo требало да се преноси преко LoRa? Није доступно на каналу са подразумеваним кључем и именом. Пошаљи позицију на примарном каналу када се корисничко дугме три пута кликне. Временска зона за датуме на екрану уређаја и у евиденцији. - Аутоматски се пребацује на следећу страницу на екрану као карусел, на основу наведеног интервала. - Смер компаса на екрану изван круга увек ће указивати на север. - Окрени екран вертикално. - Премаши аутоматско откривање OLED екрана. - Колико дуго екран остаје укључен након притиска корисничког дугмета или пријема порука. - Јединице приказане на екрану уређаја. - Захтева да уређај има акцелерометар. - Подешава максималан број скокова. Подразумевано је 3, а повећање броја одобрених скокова такође повећава загушење и треба га користити опрезно. Поруке емитоване са 0 скокова неће добити потврде пријема (ACK). Доступна унапред подешена подешавања модема, подразумевана је Long Fast. Регион у коме ћете користити ваше радио уређаје. - Минимална промена растојања у метрима која ће се узети у обзир за паметно емитовање позиције. - Опциони поља за укључивање при склапању порука о позицији. Што више поља је укључено, порука ће бити већа, што доводи до дужег времена емитовања и већег ризика од губитка пакета. - Спаваће све што је више могуће, за улогу трагача и сензора ово ће укључивати и лора радио. Не користите ово подешавање ако желите да користите свој уређај са мобилним апликацијама или користите уређај без корисничког дугмета. - Излаз дебаговања уживо преко серијског интерфејса, прегледајте и извозите логове уређаја са редукованим позицијама преко блутута. - Уређајем управља администратор мреже, корисник не може да приступи ниједном подешавању уређаја. Користи се за креирање заједничког кључа са удаљеним уређајем. - Серијска конзола преко Stream API-ја. Блутут повезан Повезан на радио уређај, али уређај је у стању спавања Стандардно @@ -153,13 +130,13 @@ Уређај је у стању спавања Директне поруке Директне поруке + Онемогућено Прекините везу Раскачено Датум Директно Поруке - Приказ Раздаљина @@ -168,16 +145,14 @@ Ажурирања фирмвера Чворови Документација - Двоструки додир као дугме + Измени 8 Сати Омогућено - Неусаглашеност јавних кључева Шифровање јавним кључем Метрике сензора - Окружење Грешка Достигнут је лимит циклуса рада. Не могу слати поруке тренутно, молимо вас покушајте касније. @@ -203,18 +178,10 @@ Белешке о издању Фиксна локација - Фреквенцијски слот - Пријатељски назив Генерисање QR кода Искључен Добро - - GPIO пин за A порт ротационог енкодера - GPIO пин за Б порт ротационог енкодера - GPIO пин за порт клика ротационог енкодера - GPS пријем GPIO - GPS предаја GPIO Хардвер Смер Скокова удаљено @@ -222,10 +189,8 @@ Знам шта радим. IAQ Индекс квалитета ваздуха (IAQ) као мера за одређивање квалитета ваздуха унутрашњости, мерен са Bosch BME680. Вредности се крећу у распону од 0 до 500. - Игнориши Додати '%1$s' на листу игнорисаних? - Игнориши MQTT Уклнити '%1$s' на листу игнорисаних? Квалитет ваздуха у затвореном простору (IAQ) @@ -237,7 +202,6 @@ Ширина - LED срчани откуцаји @@ -255,7 +219,6 @@ Низак ниво батерије: %1$s Мапа меша - Капацитет кеш меморије: %1$d MB\n Употреба кеш меморије: %2$d MB Меначер кеш меморије Тренутна величина кеш меморије %1$d плочице @@ -329,7 +292,6 @@ Последњи пут виђено преко MQTT-а Ресетовање базе чворова - Интервал емитовања информација о чвору Чворови Нема повезаних уређаја @@ -338,29 +300,25 @@ Без Није повезан Белешке + - Број записа - Позитиван за MQTT Океј 24 Сати 1н Опције - - Измена фреквенције Лозинка + Позиција - Заставице позиције Позиција Пакети позиција - Снага Подешавања напајња Мерни подаци о снази @@ -369,7 +327,6 @@ Подразумевано системско подешавање Примарни - Приватни кључ Информације о пројекту Обезбедите локацију телефона меш мрежи Јавни кључ @@ -384,15 +341,6 @@ Тест домета Конфигурација теста домета Поново покрени - - Режим реемитовања - Поново преноси сваку примећену поруку, ако је била на нашем приватном каналу или из друге мреже са истим LoRA параметрима. - Исто као понашање као ALL, али прескаче декодирање пакета и једноставно их поново преноси. Доступно само у Repeater улози. Постављање овога на било коју другу улогу резултираће ALL понашањем. - Игнорише пакете са нестандардним бројевима порта као што су: TAK, RangeTest, PaxCounter, итд. Поново преноси само пакете са стандардним бројевима порта: NodeInfo, Text, Position, Telemetry и Routing. - Игнорише примећене поруке из страних мрежа као LOCAL ONLY, али иде корак даље тако што такође игнорише поруке са чворова који нису већ на листи познатих чворова. - Игнорише примећене поруке из страних мрежа које су отворене или оне које не може да декодира. Поново преноси поруку само на локалне примарне/секундарне канале чвора. - Дозвољено само за улоге SENSOR, TRACKER и TAK_TRACKER, ово ће онемогућити све поновне преносе, слично као улога CLIENT_MUTE. - Регион Удаљена администрација @@ -414,26 +362,15 @@ Улога уређаја Клијент - Повезана апликација или самостални уређај за слање порука. Скривени клијент - Уређај који емитује само по потреби ради прикривености или уштеде енергије. Клијент мутиран - Уређај који не прослеђује пакете примљене од других уређаја. Изгубљено и нађено Поновљач - Инфраструктурни чвор за проширење покривености мреже прослеђивањем порука са минималним трошковима енергије. Није видљив на листи чворова. Рутер - Комбинација и РУТЕРА и КЛИЈЕНТА. Нису намењени за мобилне уређаје. - Инфраструктурни чвор за проширење покривености мреже прослеђивањем порука. Видљив на листи чворова. Рутер са кашњењем - Инфраструктурни чвор који увек поново емитује пакете само једном, али тек након свих других режима, обезбеђујући додатно покривање за локалне кластере. Видљиво на листи чворова. Сензор - Емитује телеметријске пакете као приоритет. - Оптимизован за комуникацију са ATAK системом, смањује рутинске емисије. ТАК Трекер - Омогућава аутоматске TAK PLI емисије и смањује рутинске емисије. Трекер - Емитује пакете са GPS позицијом као приоритет. Примљена негативна потврда Нема руте @@ -442,12 +379,10 @@ RSSI Индикатор јачине примљеног сигнала RSSI је мера која се користи за одређивање нивоа снаге која се прима преко антене. Виша вредност RSSI генерално указује на јачу и стабилнију везу. Сателита - Сачувај Сачувај Скенирај - Екран укључен за Секундарни Сигурност @@ -455,10 +390,8 @@ Изабери Изабери све Пошаљи - Серијска веза Подешавања серијске везе - Сервер подешавања Подели @@ -470,15 +403,12 @@ Квалитет сигнала Приказ - Паметно позиционирање SNR Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података. Брзина - Фактор ширења Подржан Обриши Утишај - Појачање пријемника Сервер Искључен @@ -490,7 +420,6 @@ Светла Прати систем Време - Временска зона Временско ограничење Временска ознака Праћење руте @@ -514,7 +443,6 @@ 24ч 48 Сати 2н - Трансмитер укључен Подразумевано системско подешавање Метрика @@ -525,12 +453,9 @@ Некатегорисано Време рада - - Користи предефинисано подешавање Корисник Корисничка подешавања - Корисничко име преко MQTT-а Напон Обрисати тачку путање? diff --git a/core/resources/src/commonMain/composeResources/values-sv/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-sv/schema_strings.xml new file mode 100644 index 0000000000..5ef400d323 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-sv/schema_strings.xml @@ -0,0 +1,265 @@ + + + + + Blått + Ström + Grönt + LED-läge + Rött + Förvald + CODEC2 samplingshastighet + CODEC 2 aktiverat + I2S-data ut + I2S-klocka + I2S-data in + I2S ordval + PTT pin + Bluetooth är aktiverad + Parkopplingsläge + Tillbaka + Avbryt + Ingen + Välj + Skicka ljudavisering + Detektionssensor aktiverad + GPIO-pin att övervaka + Visningsnamn + Skicka ljudavisering med larmmeddelande + Hög + Använd INPUT_PULLUP-läge + GPIO för knapp + GPIO för summer + Inaktivera trippeltryck + Dubbeltryck som knapptryck + Dubbelklick på supporterad accelerometer räknas som användarknapp. + LED pulsering + Sändningsintervall för nod-info + Återutsändningsläge + Alla + Hoppa över all avkodning + Vidarebefordra alla mottagna meddelanden med samma lora inställningar utan avkodning. Endast valbar som REPEATER. Om vald med annan roll används ALL. + Endast kärnportnummer + Endast kända + Endast lokalt + Ignorerar mottagna meddelanden från okända kanaler som är öppna eller krypterade. Vidarebefordrar endast meddelanden för nodens primära och sekundära kanaler. + Ingen + Endast för SENSOR, TRACKER och TAK_TRACKER. Stoppar all annan vidarebefordran av meddelanden. + Enhetens roll + Client + Client Base + Client Hidden + Nod som endast kommunicerar vid behov för att gömma sig och samtidigt hålla nere strömförbrukningen. + Client Mute + Nod som inte vidarebefordrar meddelanden. + Hittegods + Skickar regelbundet ut GPS position på standardkanalen för att assistera vid uppsökande. + Repeater + Router + Router Client + Router Late + Sensor + Nod som prioriterar telemetri meddelanden. + TAK + Roll optimerad för användning tillsammans med ATAK. + TAK Tracker + Skickar automatiskt ut GPS position för användning med ATAK. + Tracker + Nod som prioriterar GPS meddelanden. + Tidszon + Växlar automatiskt till nästa sida på skärmen som en karusell, baserat på angivet intervall. + Peka alltid mot norr + Kompassens visare utanför cirkeln kommer alltid att peka mot norr. + Kompassriktning + Visningsläge + Åsidosätt standard skärmlayout. + Vänd skärmen + Vänd skärmen vertikalt. + Fetstil för rubriktext + Gör rubriker på skärmen feta. + OLED-typ + Åsidosätt automatisk OLED-skärmdetektering. + Håll skärmen tänd + Hur länge skärmen är på efter att användarknappen tryckts in eller ett meddelande tagits emot. + Visa enheter + Enheter som visas på enhetens skärm. + Använd 12-timmarsformat + Visar tiden i 12-timmarsformat när denna är aktiverad. + Vakna vid tryck eller rörelse + Kräver att det finns en accelerometer på din enhet. + Utmatnings-LED aktiv hög + Larmavisering med LED + Larmavisering med summer + Larmavisering med vibration + Larmmeddelande LED + Larmmeddelande-summer + Larmmeddelande vibration + Externa aviseringar aktiverad + Time-out för påminnelse + Utmatning LED (GPIO) + Utmatning summer (GPIO) + Utmatning vibration (GPIO) + Använd I2S som summer + Använd PWM-summern + Bandbredd + Frekvens-slot + Din nods driftfrekvens beräknas baserat på regionen, modeminställning och detta fält. När värdet är 0 beräknas det automatiskt baserat på det primära kanalnamnet och kommer att bli annan än den förvalda publika frekvensen. Ändra tillbaka till den publika standardfrekvensen om privata primära och publika sekundära kanaler är konfigurerade. + Kodningshastighet + Ok till MQTT + Antal hopp + Sätter det maximala antalet hopp, standard är 3. Ökat antal hopp ökar också trängseln och bör användas med försiktighet. 0 hopp sända meddelanden kommer inte att få ACKs. + Ignorera MQTT + Förval + Lite - Fast + Lång räckvidd - snabbt + Lång räckvidd - måttligt + Lång räckvidd - långsamt + Lång räckvidd - supersnabbt + Medellång räckvidd - snabbt + Medellång räckvidd - långsamt + Kort räckvidd - snabbt + Kort räckvidd - långsamt + Kort räckvidd - supersnabbt + Mycket lång räckvidd - långsamt + Ersätt gräns för driftsperiod + Åsidosätt + Region + Regionen där du kommer att använda din radio. + Spridningsfaktor + RX förstärkt gain + Sändning aktiverad + Sändningseffekt + Använd förinställning + Spaning + Sjukvårdare + Prickskytt + Gruppledare + Gruppmedlem + Meddelande + Adress + MQTT är aktiverat + Kryptering aktiverad + Din nod kommer periodvis skicka ett okrypterat paket till den konfigurerade MQTT-servern som inkluderar id, kort och långt namn, ungefärlig plats, hardvarumodell, enhetsroll, mjukvaru-version, LoRa-region, modeminställning och den primära kanalens namn. + Lösenord + Proxy till klient aktiverad + Rotämne (root topic) + TLS aktiverat + Användarnamn + Grannskapsinformation aktiverat + Skicka över LoRa + Ange om NeighborInfo ska skickas ut över LoRa utöver igenom MQTT och PhoneAPI. Ej applicerbart på kanalen med standard namn och nyckel. + Intervall för hämtning av GPS-position + IPv4-läge + Aktivera UDP-sändning över det lokala nätverket. + Ethernet är aktiverat + Aktivering av Ethernet kommer att inaktivera Bluetooth-anslutningen till appen. TCP-nodanslutningar är inte tillgängliga på Apple-enheter. + DNS + Gateway + Ip-adress + Subnät + NTP-server + Ingen + UDP-sändning + rsyslog-server + Aktivering av WiFi kommer att inaktivera Bluetooth-anslutningen till appen. + Lösenord + SSID + PAX-räknare aktiverad + Intervall för hämtning av GPS-position + Smart distans + Minsta förändring av position för att smart position ska sända (meter). + Smart intervall + Fast plats + GPS EN GPIO + GPS-läge (fysisk maskinvara) + Intervall för hämtning av GPS-position + Hur ofta ska vi försöka få en GPS-position (<10sec håller GPS aktiv). + Aktiverad + Sändningsintervall + Längsta tid som kan förflyta utan att en nod sänder en position. + Smart position + Positionsflaggor + Valfria fält att inkludera vid sammansättning av positionsmeddelanden. Ju fler fält som väljs, desto större kommer meddelandet att bli. Längre meddelanden leder till högre sändningsutnyttjande och en högre risk för paketförlust. + Altitud + Altitud är medelhavsnivå + Geoidhöjd + Rörelseriktning + Antal satelliter + Sekvensnummer + Rörelsehastighet + Tidsstämpel + GPIO för GPS-mottagning + GPIO för GPS-sändning + Aktivera strömsparläge + Kommer att pausa allt så mycket som möjligt. För tracker- och sensor-rollen kommer detta också att omfatta lora radio. Använd inte den här inställningen om du vill använda enheten med telefonapparna eller använder en enhet utan en hårdvaruknapp. + Stäng av vid strömförlust + Vänta in Bluetooth (sekunder) + Räckvidstest aktiverat + Spara .CSV i enheten (endast ESP32) + Admin-nyckel + Den publika nyckeln som ger rätt att skicka administratörsmeddelanden till den här noden. + API för debugloggen igång + Skriv felsökningsloggar över seriell kommunikation samt visa och exportera positions-rensade loggar över Bluetooth. + Hanterat läge + Enheten hanteras av en mesh-administratör. Användaren kan inte ändra enhetsinställningarna. + Privat nyckel + Används för att skapa en delad nyckel med en fjärrnod + Publik nyckel + Seriell konsol + Seriell kommunikation över Stream API. + Den seriella kommunikationens hastighet + Den seriella kommunikationens hastighet + Eko aktiverad + Seriell aktiverad + Seriellt läge + RX + Förvald + Förvald + Timeout + TX + Lagra & Vidarebefordra aktiverad + Pulsera + Maxstorlek för historik + Returfönstrets storlek för historik + Server + Antal poster + Roll + Blått + Brun + Turkos + Mörkblå + Mörkgrön + Grönt + Magenta + Orange + Lila + Rött + Vit + Gul + Luftkvalitetsmätarmodul aktiverad + Uppdateringsintervall för luftkvalitet + Skicka enhetstelemetri + Uppdateringsintervall för enhetsdata + Miljömätvärden använder Fahrenheit + Mätmodul för miljö aktiverad + Visning av miljödata på skärm är aktiverad + Uppdateringsintervall för miljödata + Strömmätarmodul aktiverad + Visning av strömvärden på skärm är aktiverad + Uppdateringsintervall för strömmätare + diff --git a/core/resources/src/commonMain/composeResources/values-sv/strings.xml b/core/resources/src/commonMain/composeResources/values-sv/strings.xml index 4014b5f128..e9168d2031 100644 --- a/core/resources/src/commonMain/composeResources/values-sv/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sv/strings.xml @@ -17,17 +17,18 @@ --> + Luftfuktighet %1$s: %2$s Meddelande från %1$s: %2$s %1$s bort favorit - %1$d hopp bort hördes senast %1$s offline online roll %1$s mottagning %1$s Hopp %1$d: %2$d noder + Temperatur Om Acceptera Bekräftelser @@ -52,7 +53,6 @@ Lägg till enhet manuellt… Lägg till nätverkslager Adress - Admin-nyckel Admin-nycklar Administration Advancerat @@ -61,22 +61,12 @@ Luftkvalitet Ikonen för luftkvalitet - Luftkvalitetsmätarmodul aktiverad - Uppdateringsintervall för luftkvalitet Procent av luftrumstid använd för sändningar inom den senaste timmen. - - Larmavisering med summer - Larmavisering med LED Varningsklocka! - Larmavisering med vibration - Larmmeddelande-summer - Larmmeddelande LED - Larmmeddelande vibration Alla Tillåt indatakälla Höjd Altitud - Peka alltid mot norr Omgivande belysning Inställningar för omgivande belysning Mätdata samlas in för att hjälpa oss att förbättra Android-appen (tack), vi kommer att få anonymiserad information om användarnas beteende. Detta inkluderar kraschrapporter, skärmar som används i appen etc. @@ -105,28 +95,20 @@ Säkerhetskopiera nycklar Säkerhetskopiera & återställ Dålig - - Bandbredd Tryck Batteri Batteriets INA_2XX I2C-adress Blåtandsenheter - Blått Bluetooth Tillgängliga blåtandsenheter Bluetooth-inställningar - Bluetooth är aktiverad Konfiguration Hantera enhetsinställningarna och kanalerna trådlöst. Upptäckt Hitta och identifiera Meshtastic-enheter nära dig. Bluetooth - Fetstil för rubriktext Inställningar - Sändningsintervall - GPIO för knapp - GPIO för summer Beräknar… Kamerabehörighet Avbryt @@ -149,7 +131,6 @@ Kanal 8 Kanalfunktioner Denna kanal-URL är ogiltig och kan inte användas - Kanal Kanalnamn Kanal-URL Kanaler @@ -171,9 +152,6 @@ Meddelande från enheten Stäng Stäng markerade - CODEC 2 aktiverat - CODEC2 samplingshastighet - Kodningshastighet Minimera diagram Kommunicera med din släkt och dina vänner utan online- och mobiltjänster. @@ -185,44 +163,19 @@ Platstillstånd krävs för att visa avstånd och riktning. Denna enhet har ingen kompass. Riktning okänd. Kompassens norrläge uppåt - Kompassriktning Kompass Uppskattad area: \u00b1%1$s (\u00b1%2$s) Uppskattat område: okänd noggrannhet Läkta nycklar upptäcktes, välj OK för att återskapa. - Dubbelklick på supporterad accelerometer räknas som användarknapp. Kontrollerar den blinkande LED lampan på enheten. På dom flesta enheter kontrollerar det här en av de fyra LED lampor monterade. Laddning och GPS lamporna går inte att kontrollera. - Ange om NeighborInfo ska skickas ut över LoRa utöver igenom MQTT och PhoneAPI. Ej applicerbart på kanalen med standard namn och nyckel. Skicka en position på den primära kanalen när användarknappen är trippelklickad. Tidszon för datum på enhetens skärm och logg. Använd telefonens tidszon - Växlar automatiskt till nästa sida på skärmen som en karusell, baserat på angivet intervall. - Kompassens visare utanför cirkeln kommer alltid att peka mot norr. - Åsidosätt standard skärmlayout. - Vänd skärmen vertikalt. - Gör rubriker på skärmen feta. - Åsidosätt automatisk OLED-skärmdetektering. - Hur länge skärmen är på efter att användarknappen tryckts in eller ett meddelande tagits emot. - Enheter som visas på enhetens skärm. - Kräver att det finns en accelerometer på din enhet. - Din nods driftfrekvens beräknas baserat på regionen, modeminställning och detta fält. När värdet är 0 beräknas det automatiskt baserat på det primära kanalnamnet och kommer att bli annan än den förvalda publika frekvensen. Ändra tillbaka till den publika standardfrekvensen om privata primära och publika sekundära kanaler är konfigurerade. - Sätter det maximala antalet hopp, standard är 3. Ökat antal hopp ökar också trängseln och bör användas med försiktighet. 0 hopp sända meddelanden kommer inte att få ACKs. Tillgängliga modemförinställningar, standard är Long Fast. Regionen där du kommer att använda din radio. - Aktivering av Ethernet kommer att inaktivera Bluetooth-anslutningen till appen. TCP-nodanslutningar är inte tillgängliga på Apple-enheter. Aktivera UDP-sändning över det lokala nätverket. - Längsta tid som kan förflyta utan att en nod sänder en position. - Minsta förändring av position för att smart position ska sända (meter). - Hur ofta position ska skickas om minsta angivna förflyttning skett. - Valfria fält att inkludera vid sammansättning av positionsmeddelanden. Ju fler fält som väljs, desto större kommer meddelandet att bli. Längre meddelanden leder till högre sändningsutnyttjande och en högre risk för paketförlust. - Hur ofta ska vi försöka få en GPS-position (<10sec håller GPS aktiv). - Kommer att pausa allt så mycket som möjligt. För tracker- och sensor-rollen kommer detta också att omfatta lora radio. Använd inte den här inställningen om du vill använda enheten med telefonapparna eller använder en enhet utan en hårdvaruknapp. - Den publika nyckeln som ger rätt att skicka administratörsmeddelanden till den här noden. - Skriv felsökningsloggar över seriell kommunikation samt visa och exportera positions-rensade loggar över Bluetooth. - Enheten hanteras av en mesh-administratör. Användaren kan inte ändra enhetsinställningarna. Används för att skapa en delad nyckel med en fjärrnod. - Seriell kommunikation över Stream API. Konfiguration Ställ in Bluetooth-behörigheter Konfigurera kritiska larm @@ -270,7 +223,6 @@ Filtrera inkluderade Förinställda filter Filter - API för debugloggen igång Inga applikationsloggar finns att visa Uppdatera Exportera loggar @@ -312,7 +264,6 @@ Detaljer Detekteringssensor Inställningar för detekteringssensor - Detektionssensor aktiverad Enhet Konfiguration av enhet @@ -323,10 +274,8 @@ Enhetens mätvärden %1$s %1$s: %2$s%% - Uppdateringsintervall för enhetsdata %1$s: %2$s V Enheten i sovläge - Skicka enhetstelemetri Tema: %1$s, Språk: %2$s Daggpunkt Direktmeddelande @@ -367,22 +316,16 @@ Ej vald Stoppa sökning %1$s återstår - %1$d unika noder Visa karta Ledigt lagringutrymme %1$d - Display Enhetsskärm - Visningsläge - Visar tiden i 12-timmarsformat när denna är aktiverad. - Visa enheter Avstånd Filtrera på avstånd Filtrera nodlistan och mesh-kartan baserat på närheten till din telefon. Avståndsmätning Visa avståndet mellan din telefon och andra Meshtastic noder med kända positioner. - DNS Rensa sökning Sök dokumentation… @@ -407,35 +350,25 @@ Widget på hemskärmen Klart Visa inte igen för denna enhet - Dubbeltryck som knapptryck Meddelanden från en offentlig internetgateway vidarebefordras till det lokala nätverket. På grund av noll-hopp-policyn kommer trafiken från standardservern MQTT inte att sprida sig längre än den här enheten. Ladda ner Dubblett publik nyckel upptäckt + Dynamisk Konfigurera enkelt privata nätverk för säker och tillförlitlig kommunikation i avlägsna områden. - Eko aktiverad Ändra 8 timmar - Aktivera strömsparläge Aktiverad - - Kryptering aktiverad Publik nyckel matchar inte Kryptering med Publik nyckel Miljövärden - Miljö - Mätmodul för miljö aktiverad - Visning av miljödata på skärm är aktiverad - Uppdateringsintervall för miljödata - Miljömätvärden använder Fahrenheit Fel Gränsen för sändningscykeln har uppnåtts. Kan inte skicka meddelanden just nu, försök igen senare. Anslut & administrera Etablerar fjärranslutning… Ethernet-alternativ - Ethernet är aktiverat Ethernet IP: Utbyt position Expandera diagrammet @@ -447,7 +380,6 @@ Exportera GPX Extern avisering Inställningar för extern avisering - Externa aviseringar aktiverad Återställ till standardinställningar Ok Meshtastic %1$s @@ -458,10 +390,6 @@ Tillgängliga filer (%1$d): Lägg till ord eller regex:mönster - Inaktivera filtrering - Aktivera filtrering - Aktivera filtrering - Dölj meddelanden som innehåller filterord Dölj %1$d filtrerade Filter Filtrerad @@ -514,6 +442,7 @@ Uppdatering lyckades! Det här kan ta en stund... Uppdatering av fast programvara + %1$d% Okänt fel Okänd hårdvarumodell: %1$d Laddar upp fast programvara... @@ -522,9 +451,7 @@ Verifierar uppdatering... Väntar på att enheten ska återansluta... Fast programversion: %1$s - Fast PIN Fast plats - Vänd skärmen För mer information, se vår integritetspolicy. Fet @@ -535,10 +462,7 @@ Ledigt minne Tillgängligt systemminne som byte Frekv. - Frekvens-slot - Visningsnamn Gasmotstånd - Gateway Generera QR-kod Bekräfta område @@ -550,26 +474,15 @@ Sätt område på karta Kom igång Bra - GPIO GPIO pin - GPIO-pin att övervaka - GPS EN GPIO - GPS-läge (fysisk maskinvara) - GPIO för GPS-mottagning - GPIO för GPS-sändning - Grönt Hårdvara Hårdvarumodell Riktning - Pulsera Hjälp & dokumentation Dölj lager Dölj lösenord - Maxstorlek för historik - Returfönstrets storlek för historik Noder per hopp - Antal hopp Hopp bort Värd Värdstatistik @@ -577,18 +490,12 @@ Jag godkänner. Jag har läst och förstått ovanstående. Jag samtycker till okrypterad överföring av mina noddata via MQTT Jag vet vad jag håller på med. - I2S-klocka - I2S-data in - I2S-data ut - I2S ordval IAQ (Indoor Air Quality) relativ skala IAQ värdet mätt med Bosch BME600. Värdeintervall 0-500. Ikonbetydelser - Ignorera Lägg till '%1$s' på ignorera-listan? Din radioenhet kommer att starta om efter denna ändring. Ignorera inkommande - Ignorera MQTT Ta bort '%1$s' från ignorera-listan? Din radioenhet kommer att starta om efter denna ändring. Importera konfiguration @@ -611,7 +518,6 @@ Ip-adress IP-adress: Port: - IPv4-läge JSON-utdata aktiverad %1$s @@ -622,8 +528,6 @@ Latitud Läs mer - LED pulsering - LED-läge Äldre typ av adminkanal %1$d bibliotek @@ -683,11 +587,9 @@ Lux Hantera kartkällor Hantera kartlager - Hanterat läge Manuell positionsbegäran krävs Mesh Map - Cache-kapacitet: %1$d MB\nCache-användning: %2$d MB Cache-hanterare Aktuell cachestorlek %1$d kartdelar @@ -707,7 +609,6 @@ SQL-cache rensad för %1$s Samtycke för att dela okrypterad noddata via MQTT Genom att aktivera den här funktionen bekräftar och samtycker du till överföringen av enhetens geografiska plats i realtid över MQTT-protokollet utan kryptering. Denna platsdata kan användas för ändamål som live-kartrapportering, enhetsspårning och relaterade telemetrifunktioner. - Din nod kommer periodiskt skicka ett okrypterat paket till den konfigurerade MQTT-servern som inkluderar id, kort och långt namn, ungefärlig plats, hardvarumodell, enhetsroll, mjukvaru-version, LoRa-region, modeminställning och den primära kanalens namn. Välj nedladdningsområde Starta Hämtning bäring: %1$d° distans: %2$s @@ -753,9 +654,6 @@ Meddelanden µg/m³ Min - Minsta sändningsintervall (sekunder) - Smart distans - Smart intervall Förval Modul konfiguration Modulerna är redan upplåsta @@ -764,7 +662,6 @@ MQTT MQTT-konfiguration - MQTT är aktiverat Anslutningen misslyckades Nåbara (%1$s) TLS-handskakning misslyckades: %1$s @@ -790,14 +687,12 @@ Tystad i %1$d dagar och %2$s timmar Tystad i %1$s timmar Inte tystad - Sluta tjata efter (sekunder) Namn Namn kan inte vara tomt. Tillbaka Navigera till Granninformation Konfiguration av grannskapsinformation - Grannskapsinformation aktiverat Nätverk Ny kanal-länk mottagen Nya meddelanden här nedan @@ -809,7 +704,6 @@ Se till att du är inom räckhåll för enheten. Inga bluetooth-enheter hittades Ingen enhet vald - Hittade inga enheter Ingen tillgänglig statistik Säkerställ att du och din enhet är anslutna till samma nätverk. Ingen USB-enhet hittades. @@ -851,7 +745,6 @@ via Favoriter via MQTT Nollställ NodeDB - Sändningsintervall för nod-info Noder Noder på denna plats @@ -864,6 +757,8 @@ Ej ansluten Anteckning Anteckningar + + Meshnätverk Meshtastic använder aviseringar för att hålla dig uppdaterad om nya meddelanden och andra viktiga händelser. Du kan uppdatera dina aviseringsbehörigheter när som helst från inställningar. Aviseringar för kanal och direktmeddelanden. @@ -872,12 +767,8 @@ Avisera mottaget varnings- och larmmeddelande Avisera meddelandekvitto Nu - NTP-server - Antal poster - Ok till MQTT Okej - OLED-typ 24 timmar 1 timme @@ -894,25 +785,15 @@ Öppna Wi-Fi inställningar Alternativ Orientera mot norr - - Utmatning summer (GPIO) - Utmatningstid (millisekunder) - Utmatnings-LED aktiv hög - Utmatning LED (GPIO) - Utmatning vibration (GPIO) Överflödsmeny - Ersätt gräns för driftsperiod - Åsidosätt Paket auktoritet Balanserad - Föredra autentiserad Kompatibel - Tillåt osignerad - Autentisera paket när det är möjligt, men acceptera osignerad trafik för maximal kompatibilitet. Skyddsnivå Strikt - Kräv autentisering Aktivera strikt läge Aktivera strikt autentisering? - Parkopplingsläge Lösenord PAX @@ -924,7 +805,6 @@ W:%1$d Paxcounter Konfiguration av PAX-räknare - PAX-räknare aktiverad Periodisk positionssändning Meshtastic behöver "Närliggande enheter"-behörigheter aktiverade för att hitta och ansluta till enheter via Bluetooth. Du kan inaktivera när den inte används. @@ -945,20 +825,16 @@ %1$d sekund %1$d sekunder + Plats Ställ in från aktuell telefonplats Position aktiverad - Positionsflaggor Plats Positionspaket - Ström Ströminställningar Strömdata - Strömmätarmodul aktiverad - Visning av strömvärden på skärm är aktiverad - Uppdateringsintervall för strömmätare Exakt plats Språk Systemets standard @@ -977,12 +853,9 @@ Text Primär Periodisk sändning av position och telemetri - Privat nyckel Dela telefonens position till meshnätverket Leverantörens namn finns redan. - Proxy till klient aktiverad PSK - PTT pin Publik nyckel Publik nyckel har ändrats QR-kod @@ -1000,25 +873,14 @@ Regn (24t) Räckvidd Räckvidstest konfiguration - Räckvidstest aktiverat Reagera Starta om - - Återutsändningsläge - Vidarebefordra alla mottagna meddelanden med samma lora inställningar. - Vidarebefordra alla mottagna meddelanden med samma lora inställningar utan avkodning. Endast valbar som REPEATER. Om vald med annan roll används ALL. - Ignorerar meddelanden från icke-standard portnummer. Exempelvis: TAK, RangeTest, PaxCounters, etc. Vidarebefordrar endast standard portnummer. Exempelvis: NodeInfo, Text, Position, Telemetri och Routing. - Ignorerar mottagna meddelanden från okända meshnätverk som är öppna eller krypterade samt från noder som inte finns i nod listan. Vidarebefordrar endast meddelanden för kända kanaler. - Ignorerar mottagna meddelanden från okända kanaler som är öppna eller krypterade. Vidarebefordrar endast meddelanden för nodens primära och sekundära kanaler. - Endast för SENSOR, TRACKER och TAK_TRACKER. Stoppar all annan vidarebefordran av meddelanden. Senaste nätverksenheterna Återansluter… - Rött Uppdatera Uppdatera metadata Är du säker på att du vill skapa ny privat nyckel?\n\nNoder som tidigare har bytt nycklar med den här noden måste ta bort den gamla anslutningen och utbyta nycklar på nytt för att kunna återuppta säker kommunikation. Förnya nyckel - Region Hört %1$d relä Hört %1$d reläer @@ -1067,30 +929,17 @@ Enhetens roll Client Client Base - Hantera paket till och från favoritnoder som ROUTER_LATE och alla andra paket som CLIENT. - App uppkopplad eller fristående nod. Client Hidden - Nod som endast kommunicerar vid behov för att gömma sig och samtidigt hålla nere strömförbrukningen. Client Mute - Nod som inte vidarebefordrar meddelanden. Hittegods Repeater - Nod som utökar nätverket igenom att vidarebefordra meddelanden utan egen information. Syns ej i nod listan. Router Router Client - Kombinerad ROUTER och CLIENT. Ej för mobila noder. - Nod som utökar nätverket igenom att vidarebefordra meddelanden. Syns i nod listan. Router Late - Nod som utökar nätverket igenom att vidarebefordra meddelanden men endast efter alla noder. Syns i nod listan. Sensor - Nod som prioriterar telemetri meddelanden. TAK - Roll optimerad för användning tillsammans med ATAK. TAK Tracker - Skickar automatiskt ut GPS position för användning med ATAK. Tracker - Nod som prioriterar GPS meddelanden. - Rotämne (root topic) Misslyckad kvittens Ingen rutt @@ -1098,13 +947,10 @@ Timeout RSSI Received Signal Strength Indicator, ett mått som används för att avgöra effektnivån som togs emot av antennen. Ett högre RSSI-värde indikerar generellt en starkare och stabilare anslutning. - rsyslog-server Sat. - Spara Spara & starta om Spara - Spara .CSV i enheten (endast ESP32) Exportera räckviddspaket Sök @@ -1116,7 +962,6 @@ Skanna delad kontakts QR-kod Söker… Söker… - Håll skärmen tänd Gå till slutet Sök efter emoji... Sök meddelanden… @@ -1149,19 +994,8 @@ Vald Vald karttyp Skicka - Skicka ljudavisering - Skicka ljudavisering med larmmeddelande - Avsändarens meddelandeintervall (sekunder) - Seriell kommunikation - Den seriella kommunikationens hastighet Seriell konfiguration - Seriell konsol - Seriell aktiverad - Seriellt läge - RX - TX - Server Session aktiv Uppdatering krävs Ställ in tid @@ -1187,7 +1021,6 @@ Visa brytpunkter Stäng av Nod: %1$s - Stäng av vid strömförlust ⚠️ Detta kommer STÄNGA AV noden. Fysisk interaktion kommer att krävas för att slå på den. Signal Signalkvalité @@ -1212,7 +1045,6 @@ Använd nuvarande nodposition Hoppa över Lucka - Smart position SNR Signal-to-Noise Ratio, är ett mått som används inom kommunikation för att kvantifiera nivån av en önskad signal mot nivån av bakgrundsbrus. I Meshtastic och andra trådlösa system indikerar en högre SNR en tydligare signal som kan förbättra tillförlitligheten och kvaliteten på dataöverföringen. Fukthalt i jord @@ -1220,15 +1052,10 @@ Hastighet %1$d Km/h %1$d mph - Spridningsfaktor - SSID - Statussändningsintervall (sekunder) Statusmeddelande Håll dig uppkopplad var som helst Lagra & Vidarebefordra Lagra & Vidarebefordra konfiguration - Lagra & Vidarebefordra aktiverad - Subnät Lyckades Tid för djup strömsparläge Stöds @@ -1236,21 +1063,10 @@ Radera Tysta Ljud på - RX förstärkt gain Systeminställningar TAK (ATAK) TAK inställning - Medlemsroll - Spaning - Huvudkvarter - Voffsing - Sjukvårdare - Signalist - Prickskytt - Gruppledare - Gruppmedlem - Ospecificerad TAK-server Aktivera lokal TAK-server … @@ -1261,20 +1077,6 @@ ✗ Kör Kör: %1$s - Lagfärg - Blått - Brun - Turkos - Mörkblå - Mörkgrön - Grönt - Magenta - Orange - Lila - Rött - Ospecificerad - Vit - Gul Telemetri Telemetri konfiguration Temp @@ -1283,10 +1085,8 @@ Ljust Systemets standard Tid - Tidszon Timeout Tidsstämpel - TLS är aktiverat Växla min position Spåra rutt @@ -1317,7 +1117,6 @@ Modul aktiverad Översätt - Skicka över LoRa Transport API @@ -1330,10 +1129,10 @@ 24T 48 timmar 2V - Sändning aktiverad - Sändningseffekt Typ Skriv ett meddelande + dBm + m Systemets standard @@ -1348,8 +1147,6 @@ Ljud på Okänd Odefinierad - 0 - Intervall för hämtning av GPS-position - Uppdateringsintervall (sekunder) Uppdaterad Meddelanden från mesh-nätet kommer att skickas till internet via vilken nod som helst som är konfigurerad som gateway. Upptid @@ -1359,12 +1156,6 @@ URL måste innehålla platshållare. URL-mall USB - - Använd 12-timmarsformat - Använd I2S som summer - Använd INPUT_PULLUP-läge - Använd förinställning - Använd PWM-summern Användare Användarinställningar @@ -1372,7 +1163,6 @@ Användarinfo Användarens sträng Användarinfo - Användarnamn UV Lux via API via MQTT @@ -1380,8 +1170,6 @@ Visa på karta Visa version Spänning - Vänta in Bluetooth (sekunder) - Vakna vid tryck eller rörelse Varning Radera vägpunkt? Redigera vägpunkt diff --git a/core/resources/src/commonMain/composeResources/values-tr/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-tr/schema_strings.xml new file mode 100644 index 0000000000..d1713097e6 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-tr/schema_strings.xml @@ -0,0 +1,155 @@ + + + + + Mavi + Akım + Yeşil + LED durumu + Kırmızı + Varsayılan + CODEC2 örnekleme hızı + CODEC 2 etkinleştirildi + I2S veri çıkışı + I2S saati + I2S veri girişi + I2S kelime seçimi + PTT pini + Bluetooth Etkin + Eşleştirme Modu + CCW ile giriş olayı oluştur + CW ile giriş olayı oluştur + Basıldığında giriş olayı oluştur + Dönen kodlayıcı A portu için GPIO pini + Dönen kodlayıcı B portu için GPIO pini + Dönen kodlayıcı Basma portu için GPIO pini + İptal + Yok + Dönen kodlayıcı #1 etkinleştirildi + Zil gönder + Yukarı/Aşağı/Seçme girişi etkinleştirildi + Algılama tetikleme türü + Algılama Sensörü etkinleştirildi + İzlenecek GPIO pini + Arkadaşça isim + Uyarı mesajı ile zil gönder + INPUT_PULLUP modu kullan + Üçlü Tıklamayı Kapat + Desteklenen ivmeölçerlere çift dokunmayı kullanıcı düğmesine basma olarak değerlendirir. + Node Bilgisi Yayın Aralığı + Hepsi + ALL ile aynı davranış, ancak paketleri çözmeksizin yeniden yayınlar. Yalnızca Repeater rolünde kullanılabilir. Bunu başka herhangi bir rolde ayarlamak ALL davranışıyla sonuçlanacaktır. + Açık olan veya şifresini çözemediği yabancı ağlardan geldiği tespit edilen mesajları yok sayar. Yalnızca düğümlerin yerel birincil / ikincil kanallarında mesajı yeniden yayınlar. + Yok + Yalnızca SENSOR, TRACKER ve TAK_TRACKER rolleri için izin verilir, CLIENT_MUTE rolünden farklı olarak tüm yeniden yayınları engeller. + Gizlilik veya güç tasarrufu için sadece gerektiğinde yayın yapan cihaz. + Diğer cihazlardan gelen paketleri tekrarlamayan cihaz. + Cihazın kurtarılmasına yardımcı olmak için konumunu düzenli olarak varsayılan kanala mesaj olarak gönderir. + Öncelikli olarak telemetri paketlerini yayınlar. + Rutin yayınları azaltarak ATAK sistemi için optimize edilmiştir. + Rutin yayınları azaltarak otomatik TAK PLI yayınlarını etkinleştirir. + Öncelikli olarak konum paketlerini yayınlar. + Pusula yönü + Görüntü Modu + Ekranı Çevir + OLED Tipi + Görüntü Birimleri + 12h saat formatını kullan + Aktif edildiğinde, cihaz ekranda saati 12 saat formatında gösterecek + Çıktı LED aktif yüksek + Uyarı çanı LED + Uyarı çanı zırnı + Uyarı çanı titreşim + Uyarı mesajı LED + Uyarı mesajı zırnı + Uyarı mesajı titreşimi + Harici bildirim etkin + Çıktı LED (GPIO) + Çıktı zırnı (GPIO) + Çıktı titreşim (GPIO) + I2S'yi zırnı olarak kullan + PWM zırnı kullan + Bant genişliği + Hop Limiti + MQTT'yi Yoksay + Görev Döngüsünü Geçersiz Kıl + PA fanı devre dışı + Bölge + Mesaj + Adres + MQTT etkin + Şifreleme etkin + Şifre + Vekilden istemciye etkin + Ana konu + Kullanıcı adı + Komşu Bilgisi etkin + LoRa üzerinden ilet + MQTT ve PhoneAPI'ye göndermenin yanı sıra, NeighborInfo'muzun LoRa üzerinden iletilip iletilmeyeceğidir. Varsayılan anahtar ve ada sahip bir kanalda kullanılamaz. + IPv4 modu + Ethernet etkin + DNS + Ağ geçidi + IP + Alt ağ + NTP sunucusu + Yok + rsyslog sunucusu + Şifre + SSID + BLE RSSI eşiği (varsayılan -80) + Pax sayacı etkin + GPS Modu + Açık + Rakım + Uydu sayısı + Zaman Damgası + ADC çarpanını geçersiz kılma oranı + Güç tasarrufu modunu etkinleştir + Menzi testi etkin + .CSV'yi depolamada kaydet (sadece ESP32) + Yönetici Anahtarı + Hata ayıklama kaydı API'si etkin + Yönetilen Mod + Özel Anahtar + Genel Anahtar + Seri konsol + Seri baud hızı + Seri baud hızı + Eko etkin + Seri etkin + Seri modu + Varsayılan + Varsayılan + Zaman Aşımı + Kalp atışı + Geçmiş geri dönüş maksimum + Geçmiş geri dönüş penceresi + Sunucu + Kayıt sayısı + Rol + Mavi + Yeşil + Kırmızı + Hava kalitesi metrikleri modülü etkin + Çevre metrikleri Fahrenheit kullan + Çevre metrikleri modülü etkin + Çevre metrikleri ekran üzerinde etkin + Güç metrikleri modülü etkin + Güç metrikleri ekran üzerinde etkin + diff --git a/core/resources/src/commonMain/composeResources/values-tr/strings.xml b/core/resources/src/commonMain/composeResources/values-tr/strings.xml index 92b077ef8d..7df23b6c03 100644 --- a/core/resources/src/commonMain/composeResources/values-tr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-tr/strings.xml @@ -17,32 +17,24 @@ --> + Nem + Sıcaklık Hakkında Kabul et Mesajı sil işlemler - ADC çarpanını geçersiz kılma oranı Ekle Ekle Adres - Yönetici Anahtarı Yönetim Gelişmiş Gelişmiş Hava kalitesi ikonu - Hava kalitesi metrikleri modülü etkin Son bir saat içinde kullanılan iletim için yayın süresi yüzdesi. - - Uyarı çanı zırnı - Uyarı çanı LED Zili Çaldır! - Uyarı çanı titreşim - Uyarı mesajı zırnı - Uyarı mesajı LED - Uyarı mesajı titreşimi Hepsi Giriş kaynağına izin ver Tanımlanmamış pin erişimine izin ver @@ -62,16 +54,11 @@ Mevcut pinler Anahtarları Yedekle Kötü - - Bant genişliği Pil Pilin INA_2XX I2C adresi - BLE RSSI eşiği (varsayılan -80) - Mavi Bluetooth Bluetooth Ayarı - Bluetooth Etkin Yapılandırma Bluetooth Ayarlar @@ -94,7 +81,6 @@ Kanal 7 Kanal 8 Bu Kanal URL' si geçersiz ve kullanılamaz - Kanal Kanal Adı Kanallar @@ -103,15 +89,10 @@ Temizle Kapat - CODEC 2 etkinleştirildi - CODEC2 örnekleme hızı Pusula kuzey üstte - Pusula yönü - Desteklenen ivmeölçerlere çift dokunmayı kullanıcı düğmesine basma olarak değerlendirir. Cihaz üzerindeki yanıp sönen LED'i kontrol eder. Çoğu cihaz için bu, en fazla 4 LED'den birini kontrol edecektir, şarj ve GPS LED'leri kontrol edilemez. - MQTT ve PhoneAPI'ye göndermenin yanı sıra, NeighborInfo'muzun LoRa üzerinden iletilip iletilmeyeceğidir. Varsayılan anahtar ve ada sahip bir kanalda kullanılamaz. Yapılandırma Kritik Uyarı Yapılandır Konum İzinlerini Yapılandırın @@ -130,7 +111,6 @@ Loglarda ara… Filtre ekle Filtreler - Hata ayıklama kaydı API'si etkin Hata Ayıklama Paneli Aramayı sil Sonraki eşleşme @@ -152,8 +132,6 @@ Detaylar Algılama Sensörü Algılama Sensörü Ayarları - Algılama Sensörü etkinleştirildi - Algılama tetikleme türü Cihaz Cihaz uyku durumunda @@ -166,47 +144,33 @@ Doğrudan Amatör Telsiz Mesajlar - Ekran - Görüntü Modu - Aktif edildiğinde, cihaz ekranda saati 12 saat formatında gösterecek - Görüntü Birimleri Mesafe Mesafe Filtresi Uzaklık Ölçüsü - DNS Aramayı sil Bağlantılar MQTT Nodelar İndir + Dinamik - Eko etkin Düzenle 8 Saat - Güç tasarrufu modunu etkinleştir Açık - - Şifreleme etkin Genel Anahtar Uyuşmazlığı Genel Anahtar Şifrelemesi - Çevre - Çevre metrikleri modülü etkin - Çevre metrikleri ekran üzerinde etkin - Çevre metrikleri Fahrenheit kullan Hata Zaman limitine (duty cycle) ulaşıldı. Şu anda mesaj gönderilemiyor, lütfen daha sonra tekrar deneyin. - Ethernet etkin Konum takas et Ayarları dışa aktar Dış Bildirim Harici Bildirim Ayarı - Harici bildirim etkin Fabrika ayarları İdare Eder Favori @@ -219,50 +183,28 @@ Radyo yazılımı bu uygulamayla iletişim kurmak için çok eski. Bu konuda daha fazla bilgi için: Yazılım yükleme kılavuzumuza bakın. Yazılım güncellemesi gerekiyor. Güncelleme başarısız - Sabit PIN - Ekranı Çevir Boş Hafıza Frekans - Arkadaşça isim Gaz Direnci - Ağ geçidi - CCW ile giriş olayı oluştur - CW ile giriş olayı oluştur - Basıldığında giriş olayı oluştur QR kod oluştur İyi - GPIO pini - Dönen kodlayıcı A portu için GPIO pini - Dönen kodlayıcı B portu için GPIO pini - Dönen kodlayıcı Basma portu için GPIO pini - İzlenecek GPIO pini - Yeşil Donanım Donanım modeli İstikamet - Kalp atışı Şifreyi gizle - Geçmiş geri dönüş maksimum - Geçmiş geri dönüş penceresi Atlama Üzerinden Sunucu Sunucu Ölçümleri Katılıyorum. Ne yaptığımı biliyorum. - I2S saati - I2S veri girişi - I2S veri çıkışı - I2S kelime seçimi IAQ (İç Hava Kalitesi) Bosch BME680 tarafından ölçülen bağıl ölçekli IAQ değeri. Değer Aralığı 0–500. - Yok say Yoksay listesine '%1$s' eklensin mi? Değişiklikten sonra cihazınız yeniden başlar. Gelenleri Yoksay - MQTT'yi Yoksay Yoksay listesinden '%1$s' çıkarılsın mı? Değişiklikten sonra cihazınız yeniden başlar. Ayarları içe aktar @@ -276,12 +218,10 @@ IP IP Adresi: Bağlantı noktası: - IPv4 modu JSON çıktısı etkin Enlem - LED durumu Eski Yönetici kanalı Bu seçeneği aktif etmek şifrelemeyi devre dışı bırakır ve bu varsayılan Meshtastic ağı ile uyumsuzdur. @@ -302,10 +242,8 @@ %1$s Düğümünün pili düşük (%2$d%) Düşük pil: %1$s Lüks - Yönetilen Mod Manuel konum isteği gerekli - Önbellek Kapasitesi: %1$d MB\nÖnbellek Kullanımı: %2$d MB Önbellek Yöneticisi Önbellek doluluğu %1$d harita parçası @@ -320,8 +258,6 @@ Çevrim dışı işlemleri SQL Önbellek temizleme başarısız, ayrıntılar için logcat' e bakın %1$s için SQL önbelleği temizlendi - Harita raporlama - Harita raporlama aralığı (saniye) İndirilecek bölgeyi seçin İndirmeye başla yön: %1$d° mesafe: %2$s @@ -351,12 +287,10 @@ Gönderilmek üzere sırada Bilinmeyen Mesajlar - Minimum yayın (saniye) Modül ayarları MQTT MQTT Yapılandırması - MQTT etkin Bağlandı Bağlantı kesildi Bölge seçmelisin! @@ -366,12 +300,10 @@ 8 saat Her zaman Bildirimleri sessize al - Nag zaman aşımı (saniye) İsmi Geri Dön Komşu Bilgisi Komşu Bilgisi Ayarı - Komşu Bilgisi etkin Ağ Yeni Kanal Adresi(URL) alındı Sonraki @@ -393,20 +325,19 @@ Favorilerden MQTT yoluyla NodeDB sıfırla - Node Bilgisi Yayın Aralığı Düğümler Kaldır Hiçbiri (kapat) Yok Bağlı değil + + Amatör Telsiz Meshtastic, yeni mesajlar ve diğer önemli etkinlikler hakkında sizi bilgilendirmek için bildirimleri kullanır. Bildirim izinlerinizi istediğiniz zaman ayarlardan güncelleyebilirsiniz. Uyarı/çan alındığında bildirimler Mesaj alındığında bildirimler Şimdi - NTP sunucusu - Kayıt sayısı Tamam @@ -414,34 +345,22 @@ 1H Sadece Favoriler - - Çıktı zırnı (GPIO) - Çıktı süresi (milisaniye) - Çıktı LED aktif yüksek - Çıktı LED (GPIO) - Çıktı titreşim (GPIO) Konsol seri portunu geçersiz kıl - Görev Döngüsünü Geçersiz Kıl - PA fanı devre dışı - Eşleştirme Modu Şifre Pax sayacı Pax sayacı Ayarı - Pax sayacı etkin Pin + Konum Pozisyon etkinleştirildi Konum - Güç Güç Ayarı - Güç metrikleri modülü etkin - Güç metrikleri ekran üzerinde etkin Dil Sistem varsayılanı Yeniden sıralamak için basılı tutup sürükleyin @@ -449,11 +368,8 @@ Birincil Periyodik konum ve telemetri yayını - Özel Anahtar Ağda telefon konumunu kullan - Vekilden istemciye etkin PSK - PTT pini Genel Anahtar Açık Anahtar Değiştirildi Karekod @@ -467,17 +383,7 @@ Cihaz ayarları Mesafe Testi Menzi Test Ayarı - Menzi testi etkin Yeniden başlat - - Tespit edilen herhangi bir mesajı, özel kanalımızdaysa veya aynı LoRa parametrelerine sahip başka bir ağdan geliyorsa yeniden yayınlayın. - ALL ile aynı davranış, ancak paketleri çözmeksizin yeniden yayınlar. Yalnızca Repeater rolünde kullanılabilir. Bunu başka herhangi bir rolde ayarlamak ALL davranışıyla sonuçlanacaktır. - TAK, RangeTest, PaxCounter gibi standart olmayan portnum'ları yok sayarken sadece standart portnum'lar olan NodeInfo, Text, Position, Telemetry ve Routing'i yeniden yayınlar. - LOCAL ONLY gibi yabancı ağlardan geldiği tespit edilen mesajları yok sayar, ancak düğümün bilinen listesinde bulunmayan düğümlerden gelen mesajları da yok sayarak bir adım daha ileri gider. - Açık olan veya şifresini çözemediği yabancı ağlardan geldiği tespit edilen mesajları yok sayar. Yalnızca düğümlerin yerel birincil / ikincil kanallarında mesajı yeniden yayınlar. - Yalnızca SENSOR, TRACKER ve TAK_TRACKER rolleri için izin verilir, CLIENT_MUTE rolünden farklı olarak tüm yeniden yayınları engeller. - Kırmızı - Bölge Uzaktan Yönetim Uzaktan Donanım @@ -496,19 +402,6 @@ Varsayılana dön Zil tipi - Uygulamaya bağlı veya bağımsız mesajlaşma cihazı. - Gizlilik veya güç tasarrufu için sadece gerektiğinde yayın yapan cihaz. - Diğer cihazlardan gelen paketleri tekrarlamayan cihaz. - Mesajları minimum ek yük ile tekrarlayarak ağ kapsamını genişletmek için altyapı düğümü. Düğümler listesinde görünmez. - Hem ROUTER hem de CLIENT'ın bir kombinasyonu. Mobil cihazlar için değildir. - Mesajları tekrarlayarak ağ kapsamını genişletmek için altyapı düğümü. Düğümler listesinde görünür. - Tüm diğer modlardan sonra paketleri her zaman bir kez yeniden yayınlayan ve yerel kümeler için ek kapsama alanı sağlayan altyapı düğümü. Düğümler listesinde görünür. - Öncelikli olarak telemetri paketlerini yayınlar. - Rutin yayınları azaltarak ATAK sistemi için optimize edilmiştir. - Rutin yayınları azaltarak otomatik TAK PLI yayınlarını etkinleştirir. - Öncelikli olarak konum paketlerini yayınlar. - Ana konu - Dönen kodlayıcı #1 etkinleştirildi Negatif bir onay alındı Rota yok @@ -516,12 +409,9 @@ Zaman Aşımı RSSI Alınan Sinyal Gücü Göstergesi, anten tarafından alınan güç seviyesini belirlemek için kullanılan bir ölçüdür. Daha yüksek bir RSSI değeri genellikle daha güçlü ve daha istikrarlı bir bağlantıya işaret eder. - rsyslog sunucusu Uydular - Kaydet Kaydet - .CSV'yi depolamada kaydet (sadece ESP32) Tara İkincil @@ -533,17 +423,8 @@ Güvenlik Tümünü seç Gönder - Zil gönder - Uyarı mesajı ile zil gönder - Gönderen mesaj aralığı (saniye) - Seri - Seri baud hızı Seri Ayarı - Seri konsol - Seri etkin - Seri modu - Sunucu Bölgenizi Seçin ayarlar @@ -562,18 +443,12 @@ Yuva SNR Sinyal-Gürültü Oranı, iletişimde istenen bir sinyalin seviyesini arka plan gürültüsü seviyesine mukayese ölçmek için kullanılan bir ölçüdür. Meshtastic ve diğer kablosuz sistemlerde, daha yüksek bir SNR, veri iletiminin güvenilirliğini ve kalitesini artırabilecek daha net bir sinyale işaret eder. - SSID - Durum yayını (saniye) - Alt ağ Desteklenen Sil Sessiz Sesi aç Sunucu - Mavi - Yeşil - Kırmızı Telemetri Telemetri Ayarı Tema @@ -583,7 +458,6 @@ Zaman Zaman Aşımı Zaman Damgası - TLS etkin Konumunumu aç/kapa Rotayı Takip Et @@ -597,7 +471,6 @@ Yol izle - LoRa üzerinden ilet LoRa MQTT @@ -606,6 +479,8 @@ 2H Tip Mesaj yaz + dBm + m Sistem varsayılanı @@ -617,22 +492,14 @@ İzlenmeyen veya Altyapı Sesi aç Tanınmayan - Yukarı/Aşağı/Seçme girişi etkinleştirildi - Güncelleme aralığı (saniye) Çalışma Süresi URL - - 12h saat formatını kullan - I2S'yi zırnı olarak kullan - INPUT_PULLUP modu kullan - PWM zırnı kullan Kullanıcı Kullanıcı Ayarı Kullanıcı Kimliği Kullanıcı Karakter Dizisi - Kullanıcı adı MQTT yoluyla Voltaj Yer işaretini sil? diff --git a/core/resources/src/commonMain/composeResources/values-uk/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-uk/schema_strings.xml new file mode 100644 index 0000000000..26999a87a8 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-uk/schema_strings.xml @@ -0,0 +1,270 @@ + + + + + Синій + Поточний + Зелений + Стан світлодіоду + Червоний + За замовчуванням + Частота дискретизації CODEC2 + CODEC 2 увімкнено + Вибір виходу I2S + I2S тактування + Вибір входу I2S + Вибір слова I2S + PTT контакт + Bluetooth увімкнено + Режим створення пари + Генерувати подію при повороті ліворуч + Генерувати подію при повороті праворуч + Генерувати подію при натисканні + GPIO для енкодеру порт А + GPIO пін для енкодеру порт B + GPIO пін для кнопки енкодеру + Назад + Скасувати + Жоден + Вибрати + Енкодер #1 активований + Надіслати дзвіночок + Увімкнути керування Вгору/Вниз/Вибір + Тип тригера виявлення + Датчик виявлення увімкнено + GPIO контакт для моніторингу + Дружня назва + Надсилати дзвіночок з тривожним повідомленням + Високий + Використовувати режим INPUT_PULLUP + GPIO кнопки + GPIO гудка + Подвійний дотик як натискання кнопки + Розпізнавати подвійне постукування по корпусу як натискання кнопки користувача. + Частота мигання світлодіоду + Інтервал розсилки даних про вузол + Режим ретрансляції + Усі + Така сама поведінка, як і ВСІ (ALL), але пропускає декодування і просто пересилає їх. Доступно лише в ролі Repeater. Установка цієї опції на будь-які інші ролі призведе до поведінки ВСІ. + Лише відомі + Лише локальні + Ігнорує чужі mesh-мережі та пакети, які неможливо розшифрувати. Повторно передає повідомлення лише для власних каналів пристрою. + Жоден + Дозволяється лише для таких ролей, як SENSOR, TRACKER та TAK_TRACKER, і гальмуватиме всі перенаправлення, на відміну від ролі CLIENT_MUTE. + Роль пристрою + Клієнт + Client Base + Client Hidden + Пристрій, який передає лише у разі потреби для економії енергії або скритності. + Client Mute + Пристрій, який не пересилає пакети з інших пристроїв. + Loast and Found + Регулярно транслює координати в основний канал для полегшення пошуку пристрою. + Repeater + Router + Router Client + Router Late + Датчик + Пріоритетна передача пакетів телеметрії. + ТАК + Оптимізовано для з'єднання з системою ATAK, зменшує рутинні радіо трансляції. + TAK Tracker + Увімкнути автоматичну передачу TAK PLI та зменшити кількість звичайних трансляцій. + Трекер + Пріоритетна трансляція GPS-координат. + Часовий пояс + Інтервал гортання каруселі + Автоматично перемикає сторінки на екрані за принципом каруселі з заданим інтервалом. + Завжди вказувати на північ + Покажчик компаса за межами кола на екрані завжди вказуватиме на північ. + Орієнтація компаса + Режим екрану + Перевизначити стандартну розмітку екрану. + Перевернути екран + Перевернути екран по вертикалі. + Жирні заголовки + Виділяти текст заголовка на екрані жирним шрифтом. + Тип OLED + Ручне керування виявленням OLED-екрану. + Екран включено для + Час роботи екрана після натискання кнопки або отримання повідомлень. + Одиниці виміру + Одиниці, що показуються на екрані пристрою. + Використовувати 12-г формат часу + Якщо увімкнено, пристрій буде показувати час у 12-годинному форматі на екрані. + Прокидання дотиком або рухом + Вимагає наявність акселерометра на вашому пристрої. + Активний високий рівень світлодіода + LED-індикатор сигналу тривоги + Зумер сигналу тривоги + Вібрація сигналу тривоги + Мигання світлодіоду при повідомленнях тривоги + Звук зумеру при повідомленнях тривоги + Вібрація при повідомленнях тривоги + Зовнішні сповіщення увімкнено + Вихідний LED (GPIO) + Вихідний гудок (GPIO) + Вихід вібросигналу (GPIO) + Використовувати I2S як гудок + Використовувати зумер із ШІМ-керуванням + Ширина смуги пропускання + Слот частоти + Налаштування частотного слота. При значенні 0 частота визначається назвою вашого основного каналу. Якщо у вас приватний основний канал, але ви хочете чути публічні повідомлення на додаткових, встановіть тут номер стандартного публічного слота. + Швидкість кодування + MQTT: Готово + К-ть стрибків + Встановлює максимальну кількість стрибків, стандартно — 3. Збільшення цього значення підвищує завантаженість ефіру, тому його слід використовувати обережно. Повідомлення з 0 стрибків не отримують підтвердження ACK. + Ігнорувати MQTT + Пресети + Легкий - Швидкий + Легкий - Повільний + Велика дальність - Швидко + Велика дальність - Помірно + Мала дальність - Повільно + Long Range - Turbo + Середня дальність - Швидко + Середня дальність - Повільно + Вузький - Швидкий + Вузький - Повільний + Мала дальність - Повільно + Мала дальність - Повільно + Мала дальність - Турбо + Дуже велика дальність - Повільно + Ігнорувати обмеження завантаженості каналу + Перевизначити частоту + Вентилятор вимкнений + Регіон + Регіон, де ви будете використовувати радіо. + Показник розширення сигналу + Підсилення передачі + Передача активована + Потужність передачі + Використовувати пресет + Спостерігач + Санітар + Снайпер + Лідер команди + Член команди + Повідомлення + Адреса + MQTT увімкнений + Шифрування увімкнено + Пароль + Проксі для клієнта увімкнуто + Кореневий чат + Ім'я користувача + Інформацію про сусідів увімкнено + Передавати через LoRa + Дозволяє трансляцію NeighborInfo через LoRa. Дані про сусідів зазвичай надсилаються на MQTT та додаток, але ця опція вмикає їх передачу в ефір. Не працює на каналах зі стандартним ключем. + Інтервал опитування GPS + Режим IPv4 + Увімкнути трансляцію пакетів через UDP через локальну мережу. + Ethernet увімкнено + Увімкнення Ethernet вимкне Bluetooth-з'єднання до програми. З'єднання з вузлами через TCP недоступні на пристроях Apple. + DNS + Шлюз + ІР + Підмережа + NTP-сервер + Жоден + UDP трансляція + rsyslog-сервер + Увімкнення Wi-Fi вимкне Bluetooth-з'єднання до програми. + Пароль + SSID + RSSI поріг BLE (за замовчуванням -80) + Лічильник пристроїв активований + Інтервал опитування GPS + Інтелектуальна дистанція + Мінімальна дистанція в метрах, після подолання якої вузол оновить геопозицію. + Інтелектуальний інтервал + Фіксована позиція + GPS EN GPIO + Режим GPS (Фізичний пристрій) + Інтервал опитування GPS + Як часто слід намагатися отримати позицію GPS (<10 сек тримає GPS постійно увімкненим). + Увімкнено + Інтервал трансляції + Максимальний інтервал, після якого вузол обов'язково надсилає свої координати. + Інтелектуальне передавання позиції + Прапорці місцеположення + Додаткові поля для пакетів позиції. Чим більше полів вибрано, тим більшим буде розмір повідомлення, що збільшує час передачі в ефірі та ризик втрати пакетів. + Висота + Швидкість транспортного засобу + Мітка часу + GPIO отримання GPS + GPIO передачі GPS + Множник корекції напруги + Увімкнути енергоощадний режим + Режим глибокого сну для всіх систем. У ролях «Tracker» та «Sensor» також вимикається радіомодуль. Не вмикайте це налаштування, якщо вам потрібен постійний зв'язок із додатком або якщо пристрій не має фізичної кнопки для пробудження. + Вимкнути при втраті живлення + Тривалість очікування Bluetooth + Тест на відстань увімкнений + Зберегти .CSV у сховищі (лише ESP32) + Ключ адміністратора + Публічний ключ уповноважений надсилати повідомлення адміністратора на цей вузол. + API журналу відладки увімкнено + Виведення налагоджувальних логів у реальному часі через Serial; перегляд та експорт логів із прихованими координатами через Bluetooth. + Керований режим + Пристрій керується адміністратором мережі, доступ до налаштувань для користувача обмежено. + Приватний ключ + Відкритий ключ + Серійна консоль + Послідовна консоль через Stream API. + Швидкість послідовного порту + Швидкість послідовного порту + Відлуння активоване + Послідовний порт увімкнено + Послідовний режим + RX пін + За замовчуванням + За замовчуванням + Таймаут + TX пін + Пульсація + Максимальний обсяг історії + Максимальне вікно історії + Сервер + Кількість записів + Роль + Синій + Коричневий + Ціановий + Темно синій + Темно зелений + Зелений + Фіолетовий + Бордовий + Помаранчевий + Пурпурний + Червоний + Бірюзовий + Білий + Жовтий + Модуль показників якості повітря увімкнено + Інтервал оновлення показників якості повітря + Надсилати телеметрію пристрою + Інтервал оновлення показників пристрою + Екологічні показники використовують шкалу Фаренгейта + Модуль екологічних показників увімкнено + Екологічні показники на екрані увімкнено + Інтервал оновлення екологічних показників + Модуль показників потужності ввімкнено + Показники потужності на екрані ввімкнено + Інтервал оновлення показників потужності + diff --git a/core/resources/src/commonMain/composeResources/values-uk/strings.xml b/core/resources/src/commonMain/composeResources/values-uk/strings.xml index 33bedee7bf..41d1092cbd 100644 --- a/core/resources/src/commonMain/composeResources/values-uk/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-uk/strings.xml @@ -17,14 +17,15 @@ --> + Вологість %1$s: %2$s Повідомлення від %1$s: %2$s %1$s від вас - %1$d стрибків офлайн онлайн роль %1$s сигнал %1$s + Температура Про Прийняти Подяки @@ -38,7 +39,6 @@ Перекласти повідомлення Дії Корекція множника напруги - Множник корекції напруги Додати Додати приватну нотатку… @@ -47,7 +47,6 @@ Додати Додати пристрій вручну… Адреса - Ключ адміністратора Ключ адміністратора Адміністрування Розширені @@ -56,24 +55,14 @@ Якість повітря Іконка якості повітря - Модуль показників якості повітря увімкнено - Інтервал оновлення показників якості повітря Відсоток ефірного часу, використаного для передачі за останню годину. Завантаженість ефіру - - Зумер сигналу тривоги - LED-індикатор сигналу тривоги Символ звукового сповіщення! - Вібрація сигналу тривоги - Звук зумеру при повідомленнях тривоги - Мигання світлодіоду при повідомленнях тривоги - Вібрація при повідомленнях тривоги Усі Дозволити джерело введення Дозволити доступ до невизначених пінів Висота над рівнем моря Висота - Завжди вказувати на північ Фонова підсвітка Конфігурація фонової підсвітки Аналітика збирається для того, щоб допомогти нам покращити додаток для Android (дякуємо), ми будемо отримувати анонімну інформацію про поведінку користувачів. Це включає звіти про збої, екрани, що використовуються в програмі й тому подібне. @@ -99,26 +88,17 @@ Назад Резервні ключі Поганий - - Ширина смуги пропускання Бар Батарея I2C адреса INA_2XX батареї Bluetooth пристрої - RSSI поріг BLE (за замовчуванням -80) - Синій Bluetooth Доступні Bluetooth пристрої Налаштування Bluetooth - Bluetooth увімкнено Налаштування Bluetooth - Жирні заголовки Налаштування - Інтервал трансляції - GPIO кнопки - GPIO гудка Обчислюю… Позивний Дозвіл доступу до камери @@ -130,7 +110,6 @@ Шаблонне повідомлення активоване Неможливо змінити канал, тому що радіо поки що не підключені. Будь ласка, спробуйте ще раз. Вимкнення не підтримується на цьому пристрої - Інтервал гортання каруселі Загальна завантаженість поточного каналу, включно з коректною передачею TX, прийом RX та помилкові пакети (шум). Кн @@ -144,7 +123,6 @@ Канал 8 Характеристики каналу URL-адреса цього каналу недійсна та не може бути використана - Канал Ім'я каналу URL каналу Завантаженість каналу @@ -173,52 +151,24 @@ CO₂ CO₂ Волог CO₂ Темп - CODEC 2 увімкнено - Частота дискретизації CODEC2 - Швидкість кодування Згорнути графік Спілкуйтеся з друзями та спільнотою поза мережею, навіть без мобільного зв'язку. Пеленг: %1$s Відстань: %1$s Компас північ зверху - Орієнтація компаса Компас Виявлено компрометацію ключів. Натисніть «ОК», щоб створити нові. - Розпізнавати подвійне постукування по корпусу як натискання кнопки користувача. Керування миготливим світлодіодом пристрою. На більшості плат можна керувати одним із 4 доступних світлодіодів; індикатори зарядки та GPS не підтримують керування. - Дозволяє трансляцію NeighborInfo через LoRa. Дані про сусідів зазвичай надсилаються на MQTT та додаток, але ця опція вмикає їх передачу в ефір. Не працює на каналах зі стандартним ключем. Надсилати геопозицію в основний канал при потрійному натисканні кнопки. Часовий пояс для дати на екрані та журналі пристрою. Використовувати часовий пояс телефону - Автоматично перемикає сторінки на екрані за принципом каруселі з заданим інтервалом. - Покажчик компаса за межами кола на екрані завжди вказуватиме на північ. - Перевизначити стандартну розмітку екрану. - Перевернути екран по вертикалі. - Виділяти текст заголовка на екрані жирним шрифтом. - Ручне керування виявленням OLED-екрану. - Час роботи екрана після натискання кнопки або отримання повідомлень. - Одиниці, що показуються на екрані пристрою. - Вимагає наявність акселерометра на вашому пристрої. - Налаштування частотного слота. При значенні 0 частота визначається назвою вашого основного каналу. Якщо у вас приватний основний канал, але ви хочете чути публічні повідомлення на додаткових, встановіть тут номер стандартного публічного слота. - Встановлює максимальну кількість стрибків, стандартно — 3. Збільшення цього значення підвищує завантаженість ефіру, тому його слід використовувати обережно. Повідомлення з 0 стрибків не отримують підтвердження ACK. Доступні пресети для модема, за замовчуванням — Long Fast. Регіон, де ви будете використовувати радіо. - Увімкнення Ethernet вимкне Bluetooth-з'єднання до програми. З'єднання з вузлами через TCP недоступні на пристроях Apple. Увімкнути трансляцію пакетів через UDP через локальну мережу. - Максимальний інтервал, після якого вузол обов'язково надсилає свої координати. - Мінімальна дистанція в метрах, після подолання якої вузол оновить геопозицію. - Найменший проміжок часу між розсилками координат при активному русі. - Додаткові поля для пакетів позиції. Чим більше полів вибрано, тим більшим буде розмір повідомлення, що збільшує час передачі в ефірі та ризик втрати пакетів. - Як часто слід намагатися отримати позицію GPS (<10 сек тримає GPS постійно увімкненим). - Режим глибокого сну для всіх систем. У ролях «Tracker» та «Sensor» також вимикається радіомодуль. Не вмикайте це налаштування, якщо вам потрібен постійний зв'язок із додатком або якщо пристрій не має фізичної кнопки для пробудження. - Публічний ключ уповноважений надсилати повідомлення адміністратора на цей вузол. - Виведення налагоджувальних логів у реальному часі через Serial; перегляд та експорт логів із прихованими координатами через Bluetooth. - Пристрій керується адміністратором мережі, доступ до налаштувань для користувача обмежено. Використовується для створення спільного ключа з віддаленим пристроєм. Генерується на основі вашого приватного ключа та надсилається іншим вузлам мережі, щоб вони могли обчислити спільний секретний ключ. - Послідовна консоль через Stream API. Налаштування Налаштування критичних оповіщень Налаштувати дозволи для сповіщень @@ -260,7 +210,6 @@ Фільтр включено Готові фільтри Фільтри - API журналу відладки увімкнено Оновити Експортувати журнали Панель налагодження @@ -296,8 +245,6 @@ Подробиці Датчик виявлення Налаштування датчика виявлення - Датчик виявлення увімкнено - Тип тригера виявлення Пристрій Налаштування пристрою @@ -309,10 +256,8 @@ Показники пристрою %1$s %1$s: %2$s%% - Інтервал оновлення показників пристрою %1$s: %2$s Вольт Пристрій в режимі сну - Надсилати телеметрію пристрою Тема: %1$s, Мова: %2$s Точка роси Пряме повідомлення @@ -339,20 +284,14 @@ Статус Зупинити сканування %1$s залишилось - %1$d унікальних вузлів Переглянути мапу Вільне місце %1$d - Дисплей Дисплей пристрою - Режим екрану - Якщо увімкнено, пристрій буде показувати час у 12-годинному форматі на екрані. - Одиниці виміру Відстань Фільтри відстані Вимірювання відстані - DNS Очистити пошук system ai,gemini,assistant,functions,automation,voice @@ -376,35 +315,25 @@ MQTT Вузли Готово - Подвійний дотик як натискання кнопки Завантажити Виявлено дублікат публічного ключа + Динамічна Легко налаштовуйте приватні mesh-мережі для безпечного та надійного зв'язку у віддалених районах. - Відлуння активоване Редагувати 8 Годин - Увімкнути енергоощадний режим Увімкнено - - Шифрування увімкнено Не збігаються відкритий ключ Публічний ключ не збігається із записаним ключем. Ви можете видалити вузол і дозволити йому обмінятися ключами знову, але це може свідчити про проблему з безпекою. Зв’яжіться з користувачем через інший довірений канал, щоб з’ясувати, чи була зміна ключа наслідком скидання до заводських налаштувань або іншої навмисної дії. Шифрування з відкритим ключем Показники довкілля - Середовище - Модуль екологічних показників увімкнено - Екологічні показники на екрані увімкнено - Інтервал оновлення екологічних показників - Екологічні показники використовують шкалу Фаренгейта Помилка Досягнуто обмеження заповнення каналу. Неможливо надіслати повідомлення зараз, будь ласка, спробуйте ще раз пізніше. Підключення & керування Встановлюємо віддалений сеанс… Налаштування Ethernet - Ethernet увімкнено IP Ethernet: Обмін місцеположенням Розгорнути графік @@ -415,7 +344,6 @@ Експортувати GPX Зовнішні сповіщення Налаштування зовнішніх сповіщень - Зовнішні сповіщення увімкнено Скинути до заводських налаштувань Задовільний Meshtastic %1$s @@ -424,10 +352,6 @@ Видалити '%1$s' з обраних вузлів? Додати слово або regex:pattern - Вимкнути фільтрацію - Увімкнути фільтрацію - Увімкнути фільтрацію - Приховати повідомлення, які містять слова для фільтрації Фільтри Відфільтровано Жодного фільтра не налаштовано @@ -494,9 +418,7 @@ Перевірка оновлення... Очікування на перепідключення пристрою... Версія прошивки: %1$s - Фіксований PIN Фіксована позиція - Перевернути екран Для додаткової інформації, перегляньте нашу політику конфіденційності. Жирний @@ -507,41 +429,21 @@ Вільна пам'ять Доступно системної пам'яті в байтах Част - Слот частоти - Дружня назва Опір газового сенсора - Шлюз - Генерувати подію при повороті ліворуч - Генерувати подію при повороті праворуч - Генерувати подію при натисканні Згенерувати QR-код Редагувати область Залишилось %1$s З чого почати Хороший - GPIO GPIO контакт - GPIO для енкодеру порт А - GPIO пін для енкодеру порт B - GPIO пін для кнопки енкодеру - GPIO контакт для моніторингу - GPS EN GPIO - Режим GPS (Фізичний пристрій) - GPIO отримання GPS - GPIO передачі GPS - Зелений Апаратне забезпечення Модель обладнання Курс/Напрямок - Пульсація Довідка & Документація Сховати шар Приховати пароль - Максимальний обсяг історії - Максимальне вікно історії - К-ть стрибків Стрибків Хост Показники хоста @@ -549,18 +451,12 @@ Погоджуюся. Я прочитав і розумію, текст вище. Я добровільно даю згоду на незашифровану передачу даних мого вузла через MQTT Я знаю, що роблю. - I2S тактування - Вибір входу I2S - Вибір виходу I2S - Вибір слова I2S IAQ (Якість повітря в приміщенні) відносна шкала IAQ за вимірюваннями Bosch BME680. Діапазон значень: 0–500. Значення іконок - Ігнорувати Додати '%1$s' до чорного списку? Після цієї зміни ваш пристрій перезавантажиться. Ігнорувати вхідні - Ігнорувати MQTT Видалити '%1$s' з чорного списку? Після цієї зміни ваш пристрій перезавантажиться. Імпортувати налаштування @@ -581,7 +477,6 @@ IP-адреса IP Адреса: Порт: - Режим IPv4 Вивід JSON увімкнено %1$s @@ -595,8 +490,6 @@ Широта Докладніше - Частота мигання світлодіоду - Стан світлодіоду Застарілий адмін канал %1$d Бібліотеки @@ -657,11 +550,9 @@ Виявлено слабий ключ шифрування Люкс Керування шарами мап - Керований режим Місцеположення лише за запитом Мапа мережі - Місткість кешу: %1$d МБ\nВикористання кешу: %2$d МБ Керування кешем Поточний розмір кешу %1$d плиток @@ -677,11 +568,8 @@ Управління в автономному режимі Помилка очищення кешу SQL, перегляньте logcat для деталей SQL кеш очищено для %1$s - Відображення на мапі Згода на поширення незашифрованих даних вузла через MQTT Увімкнувши цю функцію, ви визнаєте і прямо погоджуєтесь з передачею географічного розташування вашого пристрою в режимі реального часу через протокол MQTT без шифрування. Ці дані можуть використовуватися для таких цілей, як відображення розташування на мапах, відстеження пристроїв і пов'язаних з цим функцій телеметрії. - Інтервал звітування на мапі (секунди) - Ваш вузол періодично надсилатиме незашифрований пакет звіту на налаштований MQTT-сервер. Він містить: ID, повні та короткі назви, приблизне місцезнаходження, модель пристрою, роль, версію прошивки, регіон LoRa, пресет модему та назву основного каналу. Оберіть регіон завантаження Почати завантаження прийом: %1$d° відстань: %2$s @@ -728,9 +616,6 @@ Повідомлення мкг/м³ Мінімум - Мінімальний період розсилки (секунди) - Інтелектуальна дистанція - Інтелектуальний інтервал Мінімальний час в робочому режимі Пресети Налаштування модуля @@ -740,7 +625,6 @@ MQTT Налаштування MQTT - MQTT увімкнений Хост не знайдено З'єднання неможливе Брокер відхилений: %1$s @@ -749,7 +633,6 @@ Доступно (%1$s) Неможливо зв'язатися з брокером (TCP) Тайм-аут після %1$d мілісекунд - MQTT проксі на цьому телефоні Під’єднано З'єднання… Відключено @@ -771,14 +654,12 @@ Режим без звуку %1$d днів, %2$s годин Режим без звуку %1$s годин Не в безшумному режимі - Інтервал нагадувань (секунди) Ім'я Назва не може бути порожньою. Перейти назад Навігаційна інформація Інформація про сусідів Налаштування інформації про сусідів - Інформацію про сусідів увімкнено Мережа Отримано URL-адресу нового каналу Нові повідомлення нище @@ -790,7 +671,6 @@ Переконайтеся, що ви перебуваєте в зоні дії пристрою. Пристроїв Bluetooth не виявлено Не вибраний пристрій - Пристрої не знайдено Статистика відсутня Не знайдений мережевий пристрій Переконайтеся, що ви підключені до тієї ж мережі, що й пристрій. @@ -836,7 +716,6 @@ через Обране через MQTT Очищення бази вузлів - Інтервал розсилки даних про вузол Вузли Підключіться до пристрою, щоб виявити вузли поруч. @@ -849,6 +728,7 @@ Жоден Не підключено Нотатки + Сповіщення для каналу та особистих повідомлень. Сповіщення про низький заряд акумулятора підключеного пристрою. @@ -856,12 +736,8 @@ Сповіщення при отриманні сигналу тривоги/дзвінка Сповіщення про отримання повідомлень Зараз - NTP-сервер - Кількість записів - MQTT: Готово Гаразд - Тип OLED 24 Годин 1 година @@ -877,19 +753,9 @@ Бібліотеки з відкритим вихідним кодом Налаштування Орієнтація північ - - Вихідний гудок (GPIO) - Тривалість виводу (мілісекунд) - Активний високий рівень світлодіода - Вихідний LED (GPIO) - Вихід вібросигналу (GPIO) Додаткове меню Перевизначити послідовний порт - Ігнорувати обмеження завантаженості каналу - Перевизначити частоту - Вентилятор вимкнений - Режим створення пари Пароль PAX @@ -901,7 +767,6 @@ W:%1$d Лічильник пристроїв Конфігурація лічильника пристроїв - Лічильник пристроїв активований Для пошуку та підключення до пристроїв через Bluetooth програмі Meshtastic потрібен дозвіл «Пристрої поблизу». Ви можете вимкнути його, коли не користуєтеся програмою. @@ -927,20 +792,16 @@ %1$d секунд %1$d секунд + Місцезнаходження Встановити з поточного місцеположення телефону Місцезнаходження увімкнено - Прапорці місцеположення Місцезнаходження Пакет позиції - Живлення Налаштування живлення Показники живлення - Модуль показників потужності ввімкнено - Показники потужності на екрані ввімкнено - Інтервал оновлення показників потужності Працює Точна позиція Мова @@ -961,11 +822,8 @@ Текст Основний Періодична трансляція координат та телеметрії - Приватний ключ Укажіть розташування для мережі - Проксі для клієнта увімкнуто PSK - PTT контакт Відкритий ключ Відкритий ключ змінено QR код @@ -983,25 +841,14 @@ Дощ (24 год) Тест дальності Налаштування тесту дальності - Тест на відстань увімкнений Реакція Перевантажити - - Режим ретрансляції - Ретранслювати будь-яке виявлене повідомлення, якщо воно з нашого приватного каналу або з іншої мережі з такими ж параметрами LoRa. - Така сама поведінка, як і ВСІ (ALL), але пропускає декодування і просто пересилає їх. Доступно лише в ролі Repeater. Установка цієї опції на будь-які інші ролі призведе до поведінки ВСІ. - Ігнорує пакети з нестандартними номерами портів: TAK, RangeTest, PaxCounter, тощо. Ретранслює лише стандартні типи: дані вузла, текст, координати, телеметрію та маршрутизацію. - Ігнорує отримані повідомлення від чужих мереж, як-от LOCAL ONLY, але робить крок далі, також ігноруючи повідомлення від вузлів, яких немає в списку відомих вузлів. - Ігнорує чужі mesh-мережі та пакети, які неможливо розшифрувати. Повторно передає повідомлення лише для власних каналів пристрою. - Дозволяється лише для таких ролей, як SENSOR, TRACKER та TAK_TRACKER, і гальмуватиме всі перенаправлення, на відміну від ролі CLIENT_MUTE. Нещодавні пристрої в мережі Роз'єднання… - Червоний Оновити Оновити метаданні Ви впевнені, що хочете новий закритий ключ?\n\nВузли, які, можливо, раніше обмінялись ключами з цим вузлом, повинні будуть видалити даний вузол і знову обмінятись ключами щоб відновити безпечний зв'язок. Згенерувати закритий ключ - Регіон Пройшло через %1$d вузол Пройшло через %1$d вузли @@ -1049,31 +896,17 @@ Роль пристрою Клієнт Client Base - Розглядає пакети від або до улюблених вузлів так само як ROUTER_LATE, а всі інші пакети як CLIENT. - Застосунок з'єднано або автономний режим обміну повідомленнями. Client Hidden - Пристрій, який передає лише у разі потреби для економії енергії або скритності. Client Mute - Пристрій, який не пересилає пакети з інших пристроїв. Loast and Found Repeater - Інфраструктурний вузол для покриття мережі. Пересилає повідомлення з мінімальним навантаженням. Не показується в списку вузлів. Router Router Client - Комбінація ROUTER і CLIENT. Не для мобільних пристроїв. - Вузол інфраструктури для розширення покриття мережею повторними повідомленнями. Видимий у списку вузлів. Router Late - Інфраструктурний вузол, що завжди ретранслює пакети один раз після всіх інших режимів, забезпечуючи додаткове покриття для локальних груп. Показується у списку вузлів. Датчик - Пріоритетна передача пакетів телеметрії. ТАК - Оптимізовано для з'єднання з системою ATAK, зменшує рутинні радіо трансляції. TAK Tracker - Увімкнути автоматичну передачу TAK PLI та зменшити кількість звичайних трансляцій. Трекер - Пріоритетна трансляція GPS-координат. - Кореневий чат - Енкодер #1 активований Я прочитав(-ла) <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Device Role Documentation</a> і допис у блозі про <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choosing The Right Device Role</a>. Отримано негативне підтвердження @@ -1082,12 +915,9 @@ Таймаут RSSI Показник рівня потужності сигналу — вимірювання, що використовується для визначення рівня потужності, що приймається антеною. Вище значення RSSI зазвичай вказує на міцніше та стабільніше з'єднання. - rsyslog-сервер Супутники - Зберегти Зберегти - Зберегти .CSV у сховищі (лише ESP32) Експортувати rangetest пакети Сканувати @@ -1096,7 +926,6 @@ Сканувати NFC Сканування… Сканування… - Екран включено для Прокрутити донизу Пошук смайлів... Вторинний @@ -1122,19 +951,8 @@ Вибрати все Вибраний Надіслати - Надіслати дзвіночок - Надсилати дзвіночок з тривожним повідомленням - Інтервал надсилання повідомлень (секунди) - Серійний порт - Швидкість послідовного порту Налаштування послідовного порту - Серійна консоль - Послідовний порт увімкнено - Послідовний режим - RX пін - TX пін - Сервер Сесія активна Потрібне оновлення Встановити час @@ -1158,7 +976,6 @@ Показати точки маршруту Вимкнути Вузол: %1$s - Вимкнути при втраті живлення ⚠️ Це призведе до ВИМКНЕННЯ вузла. Знадобиться фізична взаємодія для його увімкнення. Сигнал Якість сигналу @@ -1173,56 +990,26 @@ Приймач Пропустити Слот - Інтелектуальне передавання позиції SNR Вологість ґрунту Температура ґрунту Швидкість %1$d км/год - Показник розширення сигналу - SSID - Інтервал трансляції стану (секунди) Статус повідомлення Залишайтесь на зв'язку будь-де - Підмережа Тривалість глибокого сну Підтримується Підтримується спільнотою Meshtastic Видалити Вимкнути звук Увімкнути звук - Підсилення передачі Системі налаштування - Спостерігач - Штаб - Собака (K9) - Санітар - Телефоніст - Снайпер - Лідер команди - Член команди - Невизначений Сервер TAK Сервер Статус %1$dB ✓ ✗ - Синій - Коричневий - Ціановий - Темно синій - Темно зелений - Зелений - Фіолетовий - Бордовий - Помаранчевий - Пурпурний - Червоний - Бірюзовий - Невизначений - Білий - Жовтий Телеметрія Налаштування телеметрії Температура @@ -1231,10 +1018,8 @@ Світла Системна Час - Часовий пояс Таймаут Мітка часу - TLS увімкнений Змінити мою позицію Маршрут @@ -1263,7 +1048,6 @@ Перекласти - Передавати через LoRa API BLE @@ -1276,10 +1060,10 @@ 24Г 48 Годин 2Т - Передача активована - Потужність передачі Тип Введіть повідомлення + дБм + м Системна @@ -1294,9 +1078,6 @@ Увімкнути звук Невпізнанно Невстановлене - 0 - Увімкнути керування Вгору/Вниз/Вибір - Інтервал опитування GPS - Інтервал оновлення (секунд) Оновлено Час роботи @@ -1305,13 +1086,7 @@ URL має містити заповнювачі. Шаблон URL USB - - Використовувати 12-г формат часу Компактне декодування кирилиці - Використовувати I2S як гудок - Використовувати режим INPUT_PULLUP - Використовувати пресет - Використовувати зумер із ШІМ-керуванням Користувач Налаштування користувача @@ -1319,7 +1094,6 @@ Дані користувача Користувацький рядок Дані користувача - Ім'я користувача УФ Люкс через API через MQTT @@ -1327,8 +1101,6 @@ Переглянути на мапі Переглянути реліз Напруга - Тривалість очікування Bluetooth - Прокидання дотиком або рухом Попередження Видалити мітку? Редагувати точку diff --git a/core/resources/src/commonMain/composeResources/values-zh-rCN/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rCN/schema_strings.xml new file mode 100644 index 0000000000..eb665dc7ef --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-zh-rCN/schema_strings.xml @@ -0,0 +1,320 @@ + + + + + 蓝 + 电流 + 绿 + LED 状态 + 红 + 默认 + CODEC2 采样率 + 启用CODEC2 + I2S 数据 OUT + I2S 时钟 + I2S 数据 IN + I2S 字选择 + PTT 引脚 + 启用蓝牙 + 固定 PIN 码 + 配对模式 + 固定 PIN 码 + 不使用 PIN 码(直接配对) + CCW时生成输入事件 + CW 时生成输入事件 + 按下时生成输入事件 + 用于旋转编码器A端口的 GPIO 引脚 + 用于旋转编码器B端口的 GPIO 引脚 + 用于旋转编码器按键端口的 GPIO 引脚 + 后退 + 取消 + 下 + 左 + 无 + 右 + 选择 + 上 + 启用旋转编码器 #1 + 发送响铃 + 启用Up/Down/select 输入 + 检测触发器类型 + 启用检测传感器 + 启用检测传感器模块,需要在装有传感器的节点和要接收检测传感器文本信息或查看检测传感器日志和图表的任何节点上启用该模块。 + 显示器的 GPIO 引脚 + 易记名称 + 发送带有警报消息的响铃声 + 发送带有警报信息的 ASCII 铃声。用于触发外部铃声通知。 + 高 + 使用 输入上拉 模式 + 按钮 GPIO + 蜂鸣器 GPIO + 禁用按钮三连击 + 双击作为按钮 + 将支持的加速度计上的双击操作视为 User 按键的按压动作。 + LED 心跳 + 控制设备上闪烁的 LED。 对大多数设备而言,这将控制最多 4 个 LED 中的一个,充电指示灯和 GPS 状态灯无法控制。 + 节点信息广播间隔 + 转播模式 + 全部 + 全部跳过解码 + 与 ALL 模式的行为相同,但跳过数据包解码,仅简单地重新广播它们。仅适用于中继器角色。在其他角色中设置此选项将表现为 ALL 模式。 + 仅核心Portnumber + 仅已识别 + 仅本地 + 忽略来自开放网状网络或无法解密的消息,仅在节点的本地主/次频道上重新广播消息。 + 无 + 仅限 SENSOR、TRACKER 和 TAK_TRACKER 角色,此模式将禁止所有重新广播,与 CLIENT_MUTE 角色类似。 + 设备角色 + 客户端 + 客户群 + 连接 App 或独立的消息发送设备。 + 客户端隐藏 + 只在需要时才广播的设备,以达到隐蔽或省电的目的。 + 客户端静默 + 不转发其他设备数据包的设备。 + 失物招领 + 定期向默认信道发送位置信息,以协助设备恢复。 + 中继 + 路由 + 路由客户端 + 仅用于塔顶或山顶的基础设施节点。 不得用于屋顶或移动节点。 需要特殊的覆盖范围。在节点列表中可见。 + 延迟时间 + 传感器 + 将遥测数据包优先广播。 + TAK + 针对 ATAK 系统通信进行优化,减少常规广播。 + TAK 追踪器 + 启用自动 TAK PLI(Position Location Information)广播,并减少常规广播。 + 追踪器 + 定位模式 - 用于作为 GPS 跟踪器。从该设备发送的定位数据包优先级较高,每两分钟广播一次。智能位置广播默认为关闭。 + 时区 + 轮播间隔 + 根据指定的时间间隔,像旋转木马一样自动切换到屏幕上的下一页。 + 总是朝北 + 屏幕上的指南针总是指向北。 + 罗盘方向 + 显示模式 + 默认 128x64 屏幕布局 + 覆盖默认屏幕布局。 + 倒置顶栏,用于双色显示 + 英制 + 公制 + 翻转屏幕 + 垂直翻转屏幕。 + 加粗标题 + 屏幕上的标题文字加粗。 + OLED 类型 + 覆盖自动OLED屏幕检测。 + 开启屏幕 + 按下用户按钮或收到消息后屏幕保持亮屏的时间。 + 显示单位 + 设备屏幕显示的单位。 + 使用 12 小时制格式 + 如果启用,设备将在屏幕上以12小时制格式显示时间。 + 点击或移动时唤醒屏幕 + 需要在设备上有一个加速计。 + 输出 LED 活动高 + 如果启用,“输出 ”引脚将被拉高,禁用则表示拉低。 + 警铃 LED + 警铃蜂鸣 + 警铃震动 + 警告消息指示灯 + 警告消息蜂鸣 + 警告消息振动 + 启用外部通知 + 超时提醒 + 输出 LED (GPIO) + 输出震动(GPIO) + 输出振动 (GPIO) + 使用 I2S 作为蜂鸣器 + 使具有本地 I2S 音频输出的设备能够通过扬声器使用 RTTTL,就像使用蜂鸣器一样。例如,T-Watch S3 和 T-Deck 就具有这种功能。 + 使用 PWM 蜂鸣器 + 使用 PWM 输出(如 RAK 蜂鸣器)代替开/关输出进行调谐。这将忽略输出、输出持续时间和激活设置,而使用设备配置蜂鸣器 GPIO 选项。 + 带宽 + 频率 + 您的节点运行频率是根据地区、调制解调器预设和此字段计算的。 当0时,插槽会自动根据主频道名称计算,并会从默认的公共插槽中改变。 如果配置了私有的主频道和公立中等频道,则更改到公共默认频道。 + 编码率 + 使用MQTT + 节点数 + 设置节点的最大次数,默认值为 3。 增加频率也会增加拥塞,应该谨慎使用。0个节点广播消息不会得到ACK。 + 忽略 MQTT + 预设 + Lite - Fast + Lite - Slow + 长距离 / 快速模式 + 长距离 / 中速模式 + 长距离 / 低速模式 + 长距离 / 高速模式 + 中距离 / 快速模式 + 中距离 / 低速模式 + 中距离 - 快速模式 + Narrow - Fast + Narrow - Slow + 短距离 / 快速模式 + 短距离 / 低速模式 + 短距离 / 高速模式 + Tiny - Fast + Tiny - Slow + 超长距离 / 低速模式 + 覆盖占空比 + 频率覆盖 + PA风扇已禁用 + 区域 + 无线电配置许可区域 + 扩散因子 + RX 增益 + 启用传输 + 发送强度 + 使用预设 + 地图发布间隔 + 转发观察员 + 医疗 + 狙击手 + 团队组长 + 团队成员 + 信息 + 地址 + 启用MQTT + 启用加密 + 您的节点将定期向配置的 MQTT 服务器发送一个未加密的地图报告包,这包括id、短和长的名称, 大致位置、硬件模型、角色、固件版本、LoRa区域、调制解调器预设和主频道名称。 + 密码 + 启用客户端代理 + 利用手机上的网络连接到 MQTT。 + 根主题 + 启用 TLS + 用户名 + 启用邻居信息 + 通过 LoRa 传输 + 是否除了发送到 MQTT 和 PhoneAPI 外,还应通过 LoRa 传输我们的邻居信息(NeighborInfo)。在具有默认密钥和名称的通道上不可用。 + GPS 轮询间隔 + IPv4模式 + 在本地网络上启用通过 UDP广播数据包。 + 启用以太网 + 启用以太网将禁用蓝牙连接。TCP节点连接在 Apple 设备上不可用。 + DNS + 网关 + IP + 子网 + NTP 服务器 + 无 + UDP 广播 + rsyslog 服务器 + 启用 WiFi 将禁用应用程序的蓝牙连接。 + 密码 + SSID + BLE RSSI 阈值(默认为-80) + 启用 Paxcount + 启用 PAX 计数器模块时,通过使用 WiFi 和蓝牙来计算经过的人数。为了使 PAX 计数器正常工作,必须将 WiFi 和蓝牙都禁用。 + GPS 轮询间隔 + 检测到人员时,我们可以隔多久发送一条消息到 Mesh + 自动距离大小 + 智能位置广播考虑的最小距离变化以米为单位。 + 自动时间间隔 + 固定位置 + GPS 使能 GPIO + GPS 模式 (物理硬件) + GPS 轮询间隔 + 应该以多长的时间尝试获取 GPS 位置(<10秒将GPS保持开启)。 + 禁用 + Enabled + 不存在 + 广播间隔 + 无节点广播位置的最大间隔。 + 智能位置 + 位置标记 + 包含位置消息时可选择包含的附加字段。包含的字段越多,消息体积就越大——这会导致占用更长的空中传输时间并增加丢包的风险。 + 海拔高度 + 海平面水准面差距高度 + 大地水准面差距高度 + 运动方向 + 卫星数 + 序列编号 + 运动速度 + 时间戳 + GPS 接收GPIO + GPS 输出 GPIO + ADC乘数修正比率 + 启用节能模式 + 尽可能让所有设备处于睡眠状态,对于跟踪器和传感器来说,这也包括 LoRa 无线电。如果您想将电台与手机 App 一起使用,或使用没有用户按钮的电台,请不要使用此设置。 + 断电时关机 + 等待蓝牙持续时间 + 启用范围测试 + 保存 CSV 到存储 (仅ESP32) + 保存包含量程测试报文详细信息的 CSV 文件,目前仅适用于配有网络服务器的 ESP32 设备。 + 发送间隔 + 该设备将按所选时间间隔发送测距信息。 + 管理员密钥 + 授权向该节点发送管理员密钥。 + 启用调试日志 API + 通过串行或蓝牙导出设备调试日志。 + 管理模式 + 设备由 Mesh 管理员管理,用户无法访问任何设备设置。 + 私钥 + 用于与远程设备连接创建的共享密钥 + 公钥 + 串口控制 + 串口控制 API + 串口波特率 + 串口波特率 + 启用Echo + 如果设置了,您发送的任何数据包都会回传到设备。 + 启用串口 + 串口模式 + 接收 + 默认 + 默认 + NMEA 位置 + 简单 + 文本消息 + 超时 + 发送 + 存储 & 已启用转发 + 心跳 + 历史记录最大返回值 + 历史记录返回窗口 + 服务器 + 记录数 + 角色 + 蓝 + 棕色 + 蓝绿色 + 深蓝色 + 深绿色 + 绿 + 品红 + 栗色 + 橙色 + 紫色 + 红 + 蓝绿色 + 白色 + 黄色 + 启用空气质量计量模块 + 空气质量计量更新间隔 + 发送设备远程数据 + 启用/禁用设备遥测模块,以将指标发送至网络。这些是标称值。拥堵的网络会根据在线节点的数量自动调整为更长的间隔。节点少于个的网络会调整为更快的间隔。 + 设备计量更新间隔 + 环境测量值使用华氏度 + 启用环境计量模块 + 屏幕显示环境指标 + 环境计量更新间隔 + 启用电源计量模块 + 在屏幕上启用电源指标 + 电量计更新间隔 + 未知包阈值 + diff --git a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml index 641bb8e71b..323b9f33e4 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -17,17 +17,21 @@ --> + 湿度 %1$s: %2$s 来自 %1$s: %2$s 的消息 %1$s 离开 收藏 - %1$d 跳数 + + %1$d 跳数 + 最后通信时间 %1$s 离线 在线 角色 %1$s 信号 %1$s 跳数 %1$d: %2$d 节点 + 温度 关于 接受 开源 @@ -44,7 +48,6 @@ 翻译消息 操作 ADC 倍数覆盖 - ADC乘数修正比率 新增 添加便笺… @@ -57,7 +60,6 @@ 手动添加设备… 添加网络图层 地址 - 管理员密钥 管理密钥 管理 高级 @@ -67,24 +69,14 @@ 空气质量 空气质量图标 空气质量指标日志 - 启用空气质量计量模块 - 空气质量计量更新间隔 过去一小时内用于传输的空中占用时间百分比。 AirUtil - - 警铃蜂鸣 - 警铃 LED 警铃字符! - 警铃震动 - 警告消息蜂鸣 - 警告消息指示灯 - 警告消息振动 全部 允许输入源 允许未定义的引脚访问 海拔 海拔 - 总是朝北 环境照明 环境亮度配置 我们收集分析数据是为了帮助改进这款安卓应用(感谢您的支持),我们会收到关于用户行为的匿名信息。这包括崩溃报告、应用中使用过的屏幕等内容。 @@ -131,20 +123,15 @@ 将公钥和私钥保存到该设备的安全加密存储中。 备份 & 还原 差 - - 带宽 气压计 电池 电池INA_2XX I2C 地址 蓝牙设备 - BLE RSSI 阈值(默认为-80) - 蓝 蓝牙 可用的蓝牙设备 蓝牙配置 蓝牙已关闭请打开蓝牙以扫描附近的设备。 - 启用蓝牙 配置 无线管理您的设备设置和信道。 发现 @@ -156,14 +143,10 @@ 已达到蓝牙扫描次数限制。请在 %1$d秒后重试。 - 加粗标题 配对失败请授权附近设备权限后重试。 配对未完成请尝试重新配对。 设置 - 广播间隔 当前信道繁忙 - 按钮 GPIO - 蜂鸣器 GPIO 正在计算…… 短号 相机权限 @@ -176,7 +159,6 @@ 启用预设消息 无法更改频道,因为装置尚未连接。请再试一次。 此设备不支持关机 - 轮播间隔 当前信道的利用情况,包括格式正确的发送(TX)、接收(RX)以及无法解码的接收(即噪声)。 Ch @@ -190,7 +172,6 @@ 频道 8 频道特征 此频道 URL 无效,无法使用 - 频道 频道名称 频道URL ChUtil @@ -231,9 +212,6 @@ 二氧化碳 二氧化碳 湿度 二氧化碳 温度 - 启用CODEC2 - CODEC2 采样率 - 编码率 折叠图表 已折叠 在没有手机服务的情况下与您的朋友和社区进行网外通信。 @@ -246,47 +224,22 @@ 此设备没有指南针传感器。方向信息不可用 此设备没有指南针传感器。方向信息不可用 罗盘总是朝北 - 罗盘方向 指南针 估计区域: \u00b1%1$s (\u00b1%2$s) 估计区域:精度未知 检测到密钥泄漏,请点击 确定 进行重新生成。 - 将支持的加速度计上的双击操作视为 User 按键的按压动作。 控制设备上的指示灯闪烁。对于大多数设备,这将控制最多 4 个指示灯,充电器和 GPS 指示灯无法控制。 - 是否除了发送到 MQTT 和 PhoneAPI 外,还应通过 LoRa 传输我们的邻居信息(NeighborInfo)。在具有默认密钥和名称的通道上不可用。 当用户按钮被点击三次时,在主通道上发送定位。 设备屏幕和日志上的日期时区。 使用手机的地方时区 - 根据指定的时间间隔,像旋转木马一样自动切换到屏幕上的下一页。 - 屏幕上的指南针总是指向北。 - 覆盖默认屏幕布局。 - 垂直翻转屏幕。 - 屏幕上的标题文字加粗。 - 覆盖自动OLED屏幕检测。 - 按下用户按钮或收到消息后屏幕保持亮屏的时间。 - 设备屏幕显示的单位。 - 需要在设备上有一个加速计。 - 您的节点运行频率是根据地区、调制解调器预设和此字段计算的。 当0时,插槽会自动根据主频道名称计算,并会从默认的公共插槽中改变。 如果配置了私有的主频道和公立中等频道,则更改到公共默认频道。 - 设置节点的最大次数,默认值为 3。 增加频率也会增加拥塞,应该谨慎使用。0个节点广播消息不会得到ACK。 该地区的预设参数仅供持证 (业余无线电) 仅限操作员使用.请启用持证业余无线电模式 (Ham) 在用户配置中进行选择。 可用的调制解调器预置,默认为 “Long Fast”。 使用电台的地区。 - 启用以太网将禁用蓝牙连接。TCP节点连接在 Apple 设备上不可用。 在本地网络上启用通过 UDP广播数据包。 - 无节点广播位置的最大间隔。 - 智能位置广播考虑的最小距离变化以米为单位。 - 如果达到最小距离,位置更新将会发送最快的。 - 包含位置消息时可选择包含的附加字段。包含的字段越多,消息体积就越大——这会导致占用更长的空中传输时间并增加丢包的风险。 - 应该以多长的时间尝试获取 GPS 位置(<10秒将GPS保持开启)。 - 尽可能让所有设备处于睡眠状态,对于跟踪器和传感器来说,这也包括 LoRa 无线电。如果您想将电台与手机 App 一起使用,或使用没有用户按钮的电台,请不要使用此设置。 该节点正在重启,将在短时间内无法连接。 - 授权向该节点发送管理员密钥。 - 通过串行或蓝牙导出设备调试日志。 - 设备由 Mesh 管理员管理,用户无法访问任何设备设置。 用来创建远程设备共享密钥 从您的私钥生成并发送到网络上的其他节点,让它们能够计算共享的密钥。 - 串口控制 API Configuration 设置蓝牙权限 配置关键警报 @@ -333,7 +286,6 @@ 筛选已包含 重置筛选 筛选器 - 启用调试日志 API 没有应用日志可以显示 刷新 导出程序日志 @@ -383,8 +335,6 @@ 详细信息 检测传感器 检测传感器配置 - 启用检测传感器 - 检测触发器类型 设备 设备配置 @@ -398,17 +348,15 @@ 设备指标 %1$s %1$s: %2$s%% - 设备计量更新间隔 %1$s: %2$s V 设备休眠中 设备存储& UI (Read-Only) - 发送设备远程数据 - 启用/禁用设备遥测模块,以将指标发送至网络。这些是标称值。拥堵的网络会根据在线节点的数量自动调整为更长的间隔。节点少于个的网络会调整为更快的间隔。 主题: %1$s, 语言: %2$s 结露点 私信 私信密钥 私聊 + 禁用 忽略 断开 已断开连接 @@ -485,22 +433,19 @@ 停止扫描 AI 分析不可用 %1$s 剩余时间 - %1$d 独立节点 + + %1$d 独立节点 + 查看地图 存储空间剩余 %1$d - 显示 设备显示 - 显示模式 - 如果启用,设备将在屏幕上以12小时制格式显示时间。 - 显示单位 距离 距离筛选器 根据靠近您的手机的位置筛选节点列表和网格地图。 距离测量 显示您的手机和其他带有位置的 Meshtastic 节点之间的距离。 - DNS 清除搜索 我们既需要仰望星空,利用systemAI和Gemini 探索智能的上限;也需要脚踏实地用 Meshtastic 这样的技术守住安全的底线 @@ -553,38 +498,28 @@ 文档 完成 此设备不再显示 - 双击作为按钮 来自公共互联网网关的消息会被转发到本地 mesh 网络。由于采用零跳策略,来自默认 MQTT 服务器的流量不会传播到该设备之外。 下载 检测到重复公钥 + 动态 轻松建立私人网格网络,以便在偏远地区进行安全和可靠的通信。 - 启用Echo 编辑 编辑网络图层源 8 小时 - 启用节能模式 启用 - - 启用加密 公钥不匹配 公钥与输入的密钥不匹配。 您可以移除该节点并让它再次交换密钥,但这可能会出现密钥泄露问题。 请通过另一个受信任的频道来联系用户,以确定密钥更改是否由于出厂重置或其他故意操作。 公钥加密 传感器指标 - 环境 - 启用环境计量模块 - 屏幕显示环境指标 - 环境计量更新间隔 - 环境测量值使用华氏度 错误 触及占空比上限。暂时无法发送消息,请稍后再试。 多次尝试后仍无法建立稳定的连接.请重新选择节点进行重试。 连接 & 管理 建立远程会话… 以太网选项 - 启用以太网 以太网 IP 地址: 使用活动主题 交换位置 @@ -598,7 +533,6 @@ 导出 TAK 数据包 外部通知 外部通知设置 - 启用外部通知 恢复出厂设置 一般 Meshtastic %1$s @@ -609,10 +543,6 @@ 可用文件 (%1$d): 添加词语或正则表达式:模式 - 禁用过滤 - 启用过滤 - 启用过滤 - 隐藏包含过滤词的消息 隐藏 %1$d 过滤 搜索节点 已过滤 @@ -724,9 +654,7 @@ 正在等待设备重启到 OTA 模式... 等待设备重新连接... 固件版本 - 固定PIN码 固定位置 - 翻转屏幕 欲了解更多信息,请参阅我们的隐私政策。 粗体 @@ -737,13 +665,7 @@ 可用内存 可用系统内存 频率 - 频率 - 易记名称 气体电阻性 - 网关 - CCW时生成输入事件 - CW 时生成输入事件 - 按下时生成输入事件 生成二维码 地图围栏 @@ -767,31 +689,17 @@ 在地图上划定范围 开始 良好 - GPIO GPIO 引脚 - 用于旋转编码器A端口的 GPIO 引脚 - 用于旋转编码器B端口的 GPIO 引脚 - 用于旋转编码器按键端口的 GPIO 引脚 - 显示器的 GPIO 引脚 - GPS 使能 GPIO - GPS 模式 (物理硬件) - GPS 接收GPIO - GPS 输出 GPIO 授予权限 - 绿 硬件 硬件型号 航向 - 心跳 帮助 & 文档 隐藏图层 隐藏密码 - 历史记录最大返回值 - 历史记录返回窗口 此窗口没有收到节点 每跳节点数 - 节点数 跃点数 主机 主机测量 @@ -799,18 +707,12 @@ 我同意。 我已阅读并理解以上内容。我自愿同意通过MQTT未加密地传输我的节点数据。 我知道自己在做什么 - I2S 时钟 - I2S 数据 IN - I2S 数据 OUT - I2S 字选择 IAQ 室内空气质量(Indoor Air Quality, IAQ):由 Bosch BME680 传感器测量的相对标尺 IAQ 值,取值范围为 0–500。 图标含义 - 忽略 添加 '%1$s' 到忽略列表? 忽略接收 - 忽略 MQTT 从忽略列表中删除 '%1$s' ? 导入配置 @@ -836,7 +738,6 @@ IP IP地址: 端口: - IPv4模式 启用JSON输出 %1$s @@ -857,8 +758,6 @@ %1$s • %2$s 网络层 更多信息 - LED 心跳 - LED 状态 旧版管理频道 %1$d 库 @@ -944,11 +843,9 @@ 照度 管理自定义瓦片源 管理地图图层 - 管理模式 需要手动定位请求 Mesh 地图 - 缓存容量: %1$d MB\n缓存使用: %2$d MB 缓存管理员 当前缓存大小 %1$d 图砖 @@ -966,11 +863,8 @@ 离线管理 清除 SQL 缓存失败,请查看 logcat 纪录 清除 %1$s 的 SQL 缓存 - 地图报告 同意通过 MQTT 分享未加密的节点数据 通过启用此功能,您确认并明确同意通过MQTT协议不加密地传输您设备的实时地理位置。 这一位置数据可用于现场地图报告、设备跟踪和相关的遥测功能。 - 地图报告间隔 (秒) - 您的节点将定期向配置的MQTT服务器发送一个未加密的地图报告数据包,这包括id、长和短的名称, 大致位置、硬件型号、角色、固件版本、LoRa区域、调制解调器预设和主频道名称。 选择下载地区 开始下载 地图样式选择 @@ -993,7 +887,6 @@ Mesh 活动邀请 接收广播信标 搜索附近设备广播的邀请 - 广播信标内容消息 最大值 %1$d 字节 附近的一个 Mesh邀请你加入 Mesh 组网加入邀请 @@ -1053,9 +946,6 @@ 消息 微克每立方米 最小值 - 最小广播时间(秒) - 自动距离大小 - 自动时间间隔 最小唤醒时间 预设 模块设定 @@ -1065,7 +955,6 @@ MQTT MQTT设置 - 启用MQTT MQTT:连接已丢失 MQTT:连接被拒绝(请检查凭据) MQTT 代理失败: %1$s @@ -1079,8 +968,6 @@ 无法通过 (TCP) 协议连接到代理服务器 等待超时 %1$d 毫秒 TLS 握手失败: %1$s - 在此手机上开启 MQTT 代理 - 此手机正在为已连接的设备中继 MQTT 流量。关闭此开关可立即切断中继,且不会更改设备本身的 MQTT 设置——当 MQTT 流量过大导致连接不堪重负时,此功能非常有用。重新打开即可恢复中继。 已连接 正在连接… 已断开连接 @@ -1103,14 +990,12 @@ %1$d 天静音, %2$s 小时 %1$s 小时静音 非静音 - 屏幕超时(秒) 名称 名称不能为空。 回溯导航 导航到 邻居信息 邻居信息设置 - 启用邻居信息 网络 收到新的频道 URL 新消息 @@ -1123,7 +1008,6 @@ 未搜索到任何蓝牙设备 未找到自定义源。 未选择设备 - 未发现任何设备 未显示任何文件。 没有可用的统计信息 没有加载地图层。 @@ -1182,7 +1066,6 @@ 通过收藏夹 通过 MQTT 重置节点数据库 - 节点信息广播间隔 节点 在此位置的节点 @@ -1200,6 +1083,8 @@ 尚未联机 备注 注 + + 网状网络 Meshtastic 使用通知来随时更新新消息和其他重要事件。您可以随时从设置中更新您的通知权限。 频道和直接消息通知。 @@ -1208,12 +1093,8 @@ 警报/铃声接收通知 消息已读回执通知 当前 - NTP 服务器 - 记录数 - 使用MQTT 确定 - OLED 类型 24 小时 1 小时 @@ -1231,23 +1112,13 @@ 打开 Wi-Fi 设置 选项 朝北 - - 输出震动(GPIO) - 输出持续时间 (毫秒) - 输出 LED 活动高 - 输出 LED (GPIO) - 输出振动 (GPIO) 溢出菜单 覆盖控制台串口端口 - 覆盖占空比 - 频率覆盖 - PA风扇已禁用 数据包身份验证 平衡模式 —优先验证身份 推荐设置拒绝来自已知签名节点的未签名降级尝试。 兼容模式 —接受未签名消息 - 在可能的情况下对数据包进行身份验证,但为了获得最大的兼容性,也会接受未签名的流量。 安全等级 严格模式 —强制要求身份验证 启用严格模式 @@ -1255,7 +1126,6 @@ 仅显示和处理经过加密身份验证的网状网络数据包。旧版节点和超大尺寸的数据包可能会因此消失。 是否启用严格身份验证? 当前连接的设备不支持数据包签名验证功能。 - 配对模式 密码 PAX @@ -1267,7 +1137,6 @@ W:%1$d 客流计数 Paxcount 配置 - 启用 Paxcount 定期广播 Meshtastic需要启用“附近的设备”权限,以便通过蓝牙查找并连接设备。不使用时,您可以将其禁用。 @@ -1285,6 +1154,7 @@ %1$d 秒 + PM1.0 PM10 PM2.5 @@ -1292,16 +1162,11 @@ 定位 根据当前手机位置设置 启用位置 - 位置标记 定位 位置数据包 - 电源 电源配置 电源计量日志 - 启用电源计量模块 - 在屏幕上启用电源指标 - 电量计更新间隔 已插电 总质量 精准位置 @@ -1323,13 +1188,10 @@ 文本 主要 定期广播位置和遥测 - 私钥 项目信息 向网格提供手机位置 服务提供商名已存在。 - 启用客户端代理 共享密钥/PSK - PTT 引脚 公钥 公钥已更改 QR 码 @@ -1347,25 +1209,14 @@ 雨量 (24小时) 距离测试 范围测试设置 - 启用范围测试 互动 重启 - - 转播模式 - 重新广播任何观察到的消息,无论是来自我们的私有频道还是具有相同 LoRa 参数的其他网状网络。 - 与 ALL 模式的行为相同,但跳过数据包解码,仅简单地重新广播它们。仅适用于中继器角色。在其他角色中设置此选项将表现为 ALL 模式。 - 忽略来自非标准端口号(如 TAK、RangeTest、PaxCounter 等)的数据包,仅重新广播标准端口号的数据包:NodeInfo、Text、Position、Telemetry 和 Routing。 - 与 LOCAL_ONLY 类似,忽略来自其他网状网络的消息,但更进一步,忽略来自不在节点已知列表中的节点的消息。 - 忽略来自开放网状网络或无法解密的消息,仅在节点的本地主/次频道上重新广播消息。 - 仅限 SENSOR、TRACKER 和 TAK_TRACKER 角色,此模式将禁止所有重新广播,与 CLIENT_MUTE 角色类似。 最近使用的网络设备 正在重新连接… - 红 刷新 刷新元数据 您确定要重新生成您的私钥吗?\n\n曾与此节点交换过密钥的节点将需要删除该节点并重新交换密钥以恢复安全通信。 重新生成私钥 - 区域 连接到的 %1$d 中继节点 @@ -1415,31 +1266,17 @@ 设备角色 客户端 客户群 - 将来自或收藏节点的数据包视为ROUTER_LATE,所有其他数据包均为CLIENT。 - 应用配对或独立使用的消息传递设备 客户端隐藏 - 只在需要时才广播的设备,以达到隐蔽或省电的目的。 客户端静默 - 不转发其他设备数据包的设备。 失物招领 中继 - 通过最低开销转发消息扩展网络覆盖的基础设施节点。不可见于节点列表。 路由 路由客户端 - 同时兼具路由器和客户端功能的设备。不适用于移动设备。 - 用于通过转发消息扩展网络覆盖范围的基础设施节点。可在节点列表中看到。 延迟时间 - 基础设施节点,总是在所有其他模式之后重新广播数据包一次,以确保本地集群的额外覆盖范围。会在节点列表中显示。 传感器 - 将遥测数据包优先广播。 TAK - 针对 ATAK 系统通信进行优化,减少常规广播。 TAK 追踪器 - 启用自动 TAK PLI(Position Location Information)广播,并减少常规广播。 追踪器 - 定位模式 - 用于作为 GPS 跟踪器。从该设备发送的定位数据包优先级较高,每两分钟广播一次。智能位置广播默认为关闭。 - 根主题 - 启用旋转编码器 #1 我已经阅读了 <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">设备角色文档</a> 这篇博客文章<a href="http://meshtastic.org/blog/choosing-the-right-device-role">选择合适的设备角色</a>。 接收到否定确认 @@ -1453,13 +1290,10 @@ 消息太大,无法发送 RSSI 接收信号强度指示(Received Signal Strength Indicator, RSSI)是一种用于测量天线接收到的信号功率的指标。较高的 RSSI 值通常表示更强、更稳定的连接。 - rsyslog 服务器 卫星 - 保存 保存 & 重启 保存 - 保存 CSV 到存储 (仅ESP32) 导出范围测试数据包 扫描 @@ -1473,7 +1307,6 @@ 扫描共享联系人二维码 正在扫描… 正在扫描… - 开启屏幕 滚动到底部 搜索Emoji…… 搜索消息… @@ -1506,19 +1339,8 @@ 选择 所选地图类型 传送 - 发送响铃 - 发送带有警报消息的响铃声 - 发件人消息间隔(秒) - 串口 - 串口波特率 串口配置 - 串口控制 - 启用串口 - 串口模式 - 接收 - 发送 - 服务器 会话处于活跃状态 请刷新以获取最新状态 设置时间 @@ -1547,7 +1369,6 @@ 显示航点 关机 节点 (%1$s - 断电时关机 ⚠️ 警告!此操作将会关闭该节点。你需要使用电源开关按键才能重启设备~ 信号 信号质量 @@ -1579,7 +1400,6 @@ 使用当前节点的位置 跳过 槽位 - 智能位置 SNR 信噪比(Signal-to-Noise Ratio, SNR)是一种用于通信领域的测量指标,用于量化目标信号与背景噪声的比例。在 Meshtastic 及其他无线系统中,较高的信噪比表示信号更加清晰,从而能够提升数据传输的可靠性和质量。 土壤湿度 @@ -1587,16 +1407,11 @@ 速度 %1$d 公里/小时 %1$d 英里每小时 - 扩散因子 - SSID - 状态广播(秒) 状态消息 随时随地保持联系 停止连接 存储 & 转发 存储 & 转发配置 - 存储 & 已启用转发 - 子网 成功 深度睡眠时间 已支持 @@ -1604,21 +1419,10 @@ 删除 静音 取消静音 - RX 增益 系统设置 TAK (ATAK) TAK 配置 - 成员角色 - 转发观察员 - 指挥中心 - Doggo (K9) - 医疗 - 无线电电话操作员 - 狙击手 - 团队组长 - 团队成员 - 未指定 TAK 服务器 启用本地 TAK 服务器 … @@ -1632,22 +1436,6 @@ 失败 运行 正在运行: %1$s - 队伍颜色 - 蓝 - 棕色 - 蓝绿色 - 深蓝色 - 深绿色 - 绿 - 品红 - 栗色 - 橙色 - 紫色 - 红 - 蓝绿色 - 未指定 - 白色 - 黄色 遥测 远程配置 温度 @@ -1656,10 +1444,8 @@ 浅色 系统默认设置 时间 - 时区 超时 时间戳 - 启用TLS 切换我的位置 追踪路径 @@ -1708,7 +1494,6 @@ 无法翻译消息 翻译模型下载失败 该消息已经是您所使用的语言 - 通过 LoRa 传输 传输方式 应用程序编程接口 @@ -1722,11 +1507,11 @@ 24 小时 48 小时 2 周 - 启用传输 - 发送强度 类型 输入一条消息 UDP 广播 + dBm + m 系统默认设置 英制 @@ -1744,9 +1529,6 @@ 取消休眠所选节点 无法识别的 未设定 - 0 - 启用Up/Down/select 输入 - GPS 轮询间隔 - 更新间隔(秒) 更新 网格中的消息将通过节点配置的网关发送到公共互联网。 正常运行时间 @@ -1757,13 +1539,7 @@ URL 模板 USB USB 权限被拒绝.请重新连接设备以重试。 - - 使用 12 小时制格式 紧凑的Cyrillic编码 - 使用 I2S 作为蜂鸣器 - 使用 输入上拉 模式 - 使用预设 - 使用 PWM 蜂鸣器 用户 用户配置 @@ -1771,7 +1547,6 @@ 用户信息 用户字符串 用户信息 - 用户名 紫外线强度 通过 API 通过 MQTT @@ -1779,8 +1554,6 @@ 查看地图 查看发行版 电压 - 等待蓝牙持续时间 - 点击或移动时唤醒屏幕 警告 删除航点? 编辑航点 diff --git a/core/resources/src/commonMain/composeResources/values-zh-rTW/schema_strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rTW/schema_strings.xml new file mode 100644 index 0000000000..39d2818781 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values-zh-rTW/schema_strings.xml @@ -0,0 +1,316 @@ + + + + + 藍色 + 目前 + 綠色 + LED狀態 + 紅色 + 默認 + CODEC2 取樣率 + 啟用 CODEC 2 + I2S 數據輸出 + I2S 時鐘 + I2S 數據輸入 + I2S WS 訊號選擇 + PTT針腳 + 藍牙已啟用 + 配對模式 + 逆時針旋轉時產生輸入事件 + 順時針旋轉時產生輸入事件 + 按下時產生輸入事件 + 旋轉編碼器 A 端 GPIO 腳位 + 旋轉編碼器 B 端 GPIO 腳位 + 旋轉編碼器 按鈕 GPIO 腳位 + 返回 + 取消 + 無 + 選擇 + 啟用旋轉編碼器#1 + 發送振鈴 + 啟用上下選擇輸入 + 偵測觸發類型 + 啟用偵測感測器 + 螢幕的 GPIO 腳位 + 顯示名稱 + 告警訊息發送提示音 + 狀態廣播間隔 + 高 + 使用輸入上拉模式 + 按鈕腳位 + 蜂鳴器腳位 + 停用三連擊 + 雙擊觸發按鈕功能 + 將支援加速度計上的雙撃行為視作按壓使用者按鍵。 + LED 心跳指示 + 節點資訊廣播間隔 + 轉發模式 + 全部 + 忽略所有傳入資料 + 與「ALL」行為相同,但會跳過封包解碼,僅重新廣播它們。此功能僅適用於中繼器角色。在其他角色上設定此功能將導致「ALL」行為。 + 僅轉發基本通訊封包 + 僅限已知節點 + 僅限本地 + 忽略來自開放的或無法解密的外部 Mesh 觀察到的訊息。僅轉播來自本地節點的主要/次要頻道的訊息。 + 無 + 僅允許 SENSOR、TRACKER 和 TAK_TRACKER 角色,與 CLIENT_MUTE 角色不同,此模式將禁止所有重新廣播行為。 + 裝置角色 + 用戶端 + 客戶端基礎模式 + 無論是連接 App 還是單機使用的訊息裝置。 + Client Hidden + 僅在需要時廣播的設備,以達到隱蔽或省電的目的。 + Client Mute + 不會轉發來自其他裝置封包的裝置。 + Lost and Found + 定期將位置以訊息廣播到預設頻道,以協助裝置恢復。 + 中繼器 + Router + Router Client + Router Late + 感測器 + 優先廣播遙測封包。 + TAK + 針對 ATAK 系統通訊進行最佳化,減少例行性廣播。 + TAK Tracker + 啟用自動 TAK PLI 廣播,並減少例行廣播。 + Repeater + 將 GPS 位置封包以最高優先級廣播。 + 時區 + 輪播間隔 + 根據指定的間隔時間,在螢幕上自動切換到下一頁(走馬燈效果)。 + 始終指向北方 + 指南針的方位標示將永久指向北邊。 + 羅盤朝向 + 顯示模式 + 覆寫預設畫面佈局。 + 英制(英里/英尺) + 公制(公里/公尺) + 翻轉畫面 + 垂直翻轉螢幕。 + 粗體字 + 將螢幕標題文字設為粗體。 + OLED 類型 + 覆寫 OLED 螢幕自動偵測。 + 螢幕開啟持續時間 + 按下按鈕或收到訊息後,螢幕持續亮起的時間。 + 顯示單位 + 裝置螢幕上顯示的單位。 + 使用12小時制 + 啟用後,裝置將在螢幕上以12小時制顯示時間。 + 輕觸或移動喚醒 + 此功能需要設備有加速度計。 + 輸出LED 高電平觸發 + 告警 LED + 告警 蜂鳴 + 告警 振動 + 警示訊息 LED + 來訊警音 + 來訊振動 + 啟用外部通知 + 持續提醒逾時 + 輸出LED(GPIO) + 輸出蜂鳴(GPIO) + 輸出振動(GPIO) + 使用 I2S 控制蜂鳴器 + 使用PWM調製的蜂鳴 + 帶寬 + 頻段槽位 + 節點工作頻率是透過地區、預設參數組和此欄位計算的。當設為 0 時,時隙將根據主頻道名稱自動計算,並會與公共預設時隙不同。若同時配置了私人主頻道和公共副頻道,請務必切換回公共預設時隙。 + 編碼速率 + 允許轉發至 MQTT + 中繼次數 + 設定訊息的最大跳數,預設為 3。注意:增加跳數將導致網路擁塞,建議謹慎使用。此外,0 跳的廣播訊息將不會收到確認 (ACK)。 + 無視MQTT + 預設配置 + Lite - Fast + Lite - Slow + Long - Fast + Long - Moderate + Long - Slow + Long - Turbo + Medium - Fast + Medium - Slow + Narrow - Fast + Narrow - Slow + Short - Fast + Short - Slow + Short - Turbo + Very Long - Slow + 覆蓋工作週期/佔空比 + 手動設定頻率 + 停用PA風扇 + 地區 + 請選擇您將使用無線電設備的地區。 + 巴西 902MHz + 中國 + 歐盟 433MHz + 歐盟 868MHz + 印度 + 日本 + 韓國 + 馬來西亞 433MHz + 馬來西亞 919MHz + 尼泊爾 865MHz + 紐西蘭 865MHz + 菲律賓 433MHz + 菲律賓 868MHz + 菲律賓 915MHz + 俄羅斯 + 新加坡 923MHz + 泰國 + 台灣 + 烏克蘭 433MHz + 烏克蘭 868MHz + 美國 + 擴頻因子 + 接收增益提升 + 啟用 LoRa 發射 + 發射功率 + 使用預設值 + 前進觀測員 (FO) + 醫療兵 + 狙擊手 + 隊長 + 隊伍成員 + 訊息: + 地址 + 啟用MQTT服務器 + 加密已啟用 + 地圖回報 + 您的節點將定期向已設定的 MQTT 伺服器傳送未加密的地圖回報封包,內容包含:節點 ID、簡短名稱與完整名稱、大約位置、硬體型號、角色、韌體版本、LoRa 地區、數據機預設值及主要頻道名稱。 + 密碼 + 啟用對客戶端的代理 + 根話題 + 已啟用 TLS + 用戶名 + 啟用鄰居資訊 + 通過Lora無線電傳輸 + 除了發送至 MQTT 和 PhoneAPI 之外,我們的 NeighborInfo 是否也透過 LoRa 發送?此功能無法在使用預設密鑰和名稱的頻道使用。 + GPS 輪詢間隔 + 第四代IP模式 + 允許透過本地網路上的 UDP 廣播封包。 + 啟用以太網 + 啟用乙太網路後,節點裝置的藍牙連線功能將會停用。此外,TCP 節點連線在 Apple 設備上無法使用。 + DNS + 閘道 + IP + 子網 + 時間伺服器 + 無 + UDP 廣播 + rsyslog伺服器 + 啟用 WiFi + 啟用 Wi-Fi 後,節點裝置的藍牙連線功能將會停用。 + 密碼 + SSID + 藍牙 RSSI 閾值(預設為-80) + 已啟用人流計數(Paxcount) + GPS 輪詢間隔 + 智慧距離 + 要觸發智慧位置廣播,節點需移動的最小距離(公尺)。 + 智慧間隔 + 固定位置 + GPS 啟用腳位 + GPS 模式(實體硬體) + GPS 輪詢間隔 + 嘗試獲取 GPS 位置的頻率(< 10 秒將保持 GPS 模組開啟)。 + 已停用 + 已啟用 + 廣播間隔 + 位置廣播的最大間隔時間。 + 智慧定位 + 位置標誌 + 位置訊息可選的附加欄位。包含的欄位越多,訊息越大,將造成空中時間拉長和封包遺失的風險增加。 + 海拔高度 + 海拔高度以平均海平面為基準 + 大地高度分離值 + 載具方位角 + 衛星數量 + 序號 + 載具速度 + 時間戳記 + GPS 接收腳位 + GPS 傳送腳位 + ADC乘數修正比率 + 啟用省電模式 + 將盡可能使所有元件進入睡眠狀態。對於 tracker 和 sensor 角色,此模式將包含 LoRa 無線電。如果您想搭配手機應用程式使用設備,或正在使用沒有使用者按鈕的設備,請勿啟用此設定。 + 電源中斷時關機 + 藍牙等待持續時間 + 啟用範圍測試 + 將 .CSV 保存到內部儲存空間(僅限ESP32) + 傳送間隔 + 管理員金鑰 + 被授權可對此節點發送管理訊息的公鑰。 + 啟用除錯日誌 API + 透過序列埠輸出即時除錯日誌;透過藍牙檢視並匯出已移除定位資訊的設備記錄。 + 託管模式 + 設備處於受管理狀態,使用者無法變更任何設備設定。 + 私鑰 + 用於與遠端裝置建立共用金鑰 + 公鑰 + 序列控制台 + 透過 API 串流的序列主控台。 + 序列埠鮑率 + 序列埠鮑率 + 啟用 Echo + 如果設定了,你傳送的任何封包都會回傳到你的裝置。 + 啟用序列埠 + 序列埠模式 + RX + 默認 + 默認 + 簡訊 + Timeout - 超時 + TX + 已啟用儲存 & 轉發 + 心跳封包 + 歴史紀錄最大返回值 + 歴史紀錄返回視窗 + 伺服器 + 紀錄數目 + 角色 + Blue - 藍色 + 咖啡色 + 天青色 + 深藍色 + 墨綠色 + Green - 綠色 + 洋紅色 + 栗紅色 + 橙色 + 紫色 + Red - 紅色 + 羽青色 + 白色 + 黃色 + 啟用空氣品質模組 + 空氣品質資訊更新間隔 + 傳送裝置遙測資料 + 啟用或停用裝置遙測模組,以控制是否將計量資料傳送至網狀網路。此處顯示的是標稱間隔值。當網狀網路出現壅塞時,系統會根據目前線上的節點數量,自動調整為更長的傳送間隔,以減輕網路負擔。 + 裝置資訊更新間隔 + 環境指標以華氏溫度顯示 + 啟用環境資訊模組 + 在螢幕上顯示環境資訊 + 環境資訊更新間隔 + 啟用電池資訊模組 + 在螢幕上顯示電量資訊 + 電源資訊更新間隔 + 不明封包閾值 + diff --git a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml index 7b76fe5c88..9cc10a3a97 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -17,16 +17,20 @@ --> + 濕度 %1$s: %2$s 來自 %1$s 的訊息:%2$s 距離 %1$s 最愛 - 距離 %1$d 跳 + + 距離 %1$d 跳 + 最後收到 %1$s 離線 線上 角色 %1$s 訊號 %1$s + 溫度 關於 接受 致謝 @@ -41,7 +45,6 @@ 顯示訊息狀態 動作 ADC 校正係數 - ADC乘數修正比率 新增 新增私人備註… @@ -54,7 +57,6 @@ 手動新增裝置… 新增線上圖層 地址 - 管理員金鑰 管理金鑰 管理 進階 @@ -62,24 +64,14 @@ 進階 空氣品質圖示 - 啟用空氣品質模組 - 空氣品質資訊更新間隔 過去一小時内傳輸所使用的通話時間(airtime)百分比。 空中時間使用率 - - 告警 蜂鳴 - 告警 LED 警鈴字符! - 告警 振動 - 來訊警音 - 警示訊息 LED - 來訊振動 全部 允許外部輸入 允許未定義腳位連接 海拔 海拔高度 - 始終指向北方 周圍光照 裝置燈光設定 我們會收集分析數據以協助改善 Android 應用程式(感謝您的支持),我們將收到匿名化的使用者行為資訊,包括當機報告、應用程式使用畫面等。 @@ -103,29 +95,20 @@ 備份金鑰 備份 & 還原 不良 - - 帶寬 氣壓 電池 電池 INA_2XX I2C 地址 藍牙裝置 - 藍牙 RSSI 閾值(預設為-80) - 藍色 藍牙 可連接的藍牙裝置 藍牙配置 - 藍牙已啟用 設定 無線管理你的裝置設定與頻道。 探索 尋找並識別附近的 Meshtastic 裝置。 藍牙 - 粗體字 設定 - 廣播間隔 - 按鈕腳位 - 蜂鳴器腳位 正在計算…… 相機權限 取消 @@ -135,7 +118,6 @@ 啟用罐頭訊息 無法更改頻道,因為裝置尚未連接。請再試一次。 此裝置不支援關機功能 - 輪播間隔 目前頻道的使用情況,包括格式正確的傳輸(TX)、接收(RX)和格式錯誤的接收(也稱為雜訊)。 頻道 @@ -149,7 +131,6 @@ 頻道 8 頻道功能 此頻道 URL 無效,無法使用 - 頻道 頻道名稱 頻道網址 頻道利用率 @@ -187,9 +168,6 @@ 客户端通知 關閉 關閉選取 - 啟用 CODEC 2 - CODEC2 取樣率 - 編碼速率 收起圖表 無需手機訊號,也能與您的朋友和社群離線通訊。 @@ -201,45 +179,20 @@ 需要位置權限才能顯示距離和方位。 此裝置沒有指南針感測器,無法取得方向資訊。 指南針北方向上 - 羅盤朝向 指南針 估計範圍: \u00b1%1$s (\u00b1%2$s) 估計範圍: 精確度未知 偵測到金鑰已洩漏,點選確定後重新產生金鑰。 - 將支援加速度計上的雙撃行為視作按壓使用者按鍵。 控制裝置上閃爍的 LED。對大多數裝置而言,這將控制最多 4 個 LED 中的一個,充電器和 GPS LED 則無法控制。 - 除了發送至 MQTT 和 PhoneAPI 之外,我們的 NeighborInfo 是否也透過 LoRa 發送?此功能無法在使用預設密鑰和名稱的頻道使用。 點擊三次 User 按鈕時,向主頻道發送位置資訊。 用於設備螢幕和記錄檔日期的時區。 使用手機時區 - 根據指定的間隔時間,在螢幕上自動切換到下一頁(走馬燈效果)。 - 指南針的方位標示將永久指向北邊。 - 覆寫預設畫面佈局。 - 垂直翻轉螢幕。 - 將螢幕標題文字設為粗體。 - 覆寫 OLED 螢幕自動偵測。 - 按下按鈕或收到訊息後,螢幕持續亮起的時間。 - 裝置螢幕上顯示的單位。 - 此功能需要設備有加速度計。 - 節點工作頻率是透過地區、預設參數組和此欄位計算的。當設為 0 時,時隙將根據主頻道名稱自動計算,並會與公共預設時隙不同。若同時配置了私人主頻道和公共副頻道,請務必切換回公共預設時隙。 - 設定訊息的最大跳數,預設為 3。注意:增加跳數將導致網路擁塞,建議謹慎使用。此外,0 跳的廣播訊息將不會收到確認 (ACK)。 可選的預設參數組,預設值是 Long Fast。 請選擇您將使用無線電設備的地區。 - 啟用乙太網路後,節點裝置的藍牙連線功能將會停用。此外,TCP 節點連線在 Apple 設備上無法使用。 允許透過本地網路上的 UDP 廣播封包。 - 位置廣播的最大間隔時間。 - 要觸發智慧位置廣播,節點需移動的最小距離(公尺)。 - 滿足最小距離限制時,位置更新的最快發送間隔。 - 位置訊息可選的附加欄位。包含的欄位越多,訊息越大,將造成空中時間拉長和封包遺失的風險增加。 - 嘗試獲取 GPS 位置的頻率(< 10 秒將保持 GPS 模組開啟)。 - 將盡可能使所有元件進入睡眠狀態。對於 tracker 和 sensor 角色,此模式將包含 LoRa 無線電。如果您想搭配手機應用程式使用設備,或正在使用沒有使用者按鈕的設備,請勿啟用此設定。 - 被授權可對此節點發送管理訊息的公鑰。 - 透過序列埠輸出即時除錯日誌;透過藍牙檢視並匯出已移除定位資訊的設備記錄。 - 設備處於受管理狀態,使用者無法變更任何設備設定。 用於與遠端設備交換密鑰。 從您的私鑰生成並傳送給網狀網路中的其他節點,以供它們計算出共享密鑰。 - 透過 API 串流的序列主控台。 設定 設定藍牙權限 設定緊急警示 @@ -287,7 +240,6 @@ 包含篩選器 預設篩選條件 篩選 - 啟用除錯日誌 API 重新整理 匯出日誌 偵錯面板 @@ -320,8 +272,6 @@ 詳情 檢測傳感器 偵測感測器設定 - 啟用偵測感測器 - 偵測觸發類型 裝置 裝置設定 @@ -332,17 +282,15 @@ 裝置計量資料 %1$s %1$s:%2$s%% - 裝置資訊更新間隔 %1$s:%2$s%V 設備休眠中 裝置儲存空間與使用者介面(唯讀) - 傳送裝置遙測資料 - 啟用或停用裝置遙測模組,以控制是否將計量資料傳送至網狀網路。此處顯示的是標稱間隔值。當網狀網路出現壅塞時,系統會根據目前線上的節點數量,自動調整為更長的傳送間隔,以減輕網路負擔。 主題 %1$s,語言 %2$s 露點 直通訊息 私訊金鑰 私訊 + 已停用 放棄變更 中斷連線 已中斷連線 @@ -355,19 +303,14 @@ 訊息 已選取 硬碟可用空間:%1$d - 顯示 裝置列表 - 顯示模式 - 啟用後,裝置將在螢幕上以12小時制顯示時間。 - 顯示單位 距離 距離篩選器 根據您手機的距離,篩選節點列表和 Mesh 網路地圖。 距離量測 顯示您手機與其他有定位資訊的 Meshtastic 節點之間的距離。 - DNS 清除搜尋結果 藍牙,usb,tcp,配對,序列,wifi @@ -400,6 +343,7 @@ MQTT 節點指標 節點 + 通知 # 入門指南 設定 — 模組 & 管理 設定 — 無線電 & 使用者 @@ -410,37 +354,27 @@ 單位 & 語系 完成 不再為此裝置顯示 - 雙擊觸發按鈕功能 網際網路閘道的訊息會轉發到本地網狀網路,但因為採用零跳躍政策,來自預設 MQTT 伺服器的流量只會到達本裝置,不會再轉發給其他節點。 下載 偵測到重複的公鑰 + 動態 輕鬆設定私有網狀網絡,以實現偏遠地區安全可靠的通訊。 - 啟用 Echo 編輯 編輯自定義圖磚來源 8 小時 - 啟用省電模式 已啟用 - - 加密已啟用 公鑰不相符 公開金鑰與先前記錄不符。您可以移除此節點並重新進行金鑰交換,但這可能代表有更嚴重的安全性問題。建議透過其他可靠的通訊方式聯繫該使用者,確認金鑰改變是否為重設裝置或其他有意的操作。 加密公鑰 環境計量資料 - 環境 - 啟用環境資訊模組 - 在螢幕上顯示環境資訊 - 環境資訊更新間隔 - 環境指標以華氏溫度顯示 錯誤 達到循環工作週期限制。目前無法發送訊息,請稍後再試。 連線 & 管理 正在建立遠端連線… 乙太網路選項 - 啟用以太網 乙太網路 IP: 交換位置 展開圖表 @@ -451,7 +385,6 @@ 匯出 TAK 資料封包 外部通知 外部通知規劃 - 啟用外部通知 恢復出廠設置 普通 Meshtastic %1$s @@ -462,10 +395,6 @@ 可使用檔案(%1$d): 新增關鍵字或正規表示式 - 停用篩選 - 啟用篩選 - 啟用篩選 - 隱藏符合篩選條件的訊息 隱藏已篩選 %1$d 則 過濾器 已篩選 @@ -528,6 +457,7 @@ 處理中,請稍候⋯⋯ 目標裝置:%1$s 韌體更新 + %1$d% 未知錯誤 未知的硬體型號: %1$d 無法識別的遠端版本 @@ -542,47 +472,25 @@ 等待裝置重新啟動至 OTA 模式⋯⋯ 等待裝置重新連線⋯⋯ 韌體版本:%1$s - 固定藍牙PIN碼 固定位置 - 翻轉畫面 如欲了解更多資訊,請查閱我們的隱私權政策。 可用記憶體 可用系統記憶體(位元組) 頻率 - 頻段槽位 - 顯示名稱 氣體感測器 - 網閘 - 逆時針旋轉時產生輸入事件 - 順時針旋轉時產生輸入事件 - 按下時產生輸入事件 產生 QR Code 開始使用 良好 - 腳位 GPIO 引脚 - 旋轉編碼器 A 端 GPIO 腳位 - 旋轉編碼器 B 端 GPIO 腳位 - 旋轉編碼器 按鈕 GPIO 腳位 - 螢幕的 GPIO 腳位 - GPS 啟用腳位 - GPS 模式(實體硬體) - GPS 接收腳位 - GPS 傳送腳位 - 綠色 硬體 硬體型號 航向 - 心跳封包 說明 & 文件 隱藏圖層 隱藏密碼 - 歴史紀錄最大返回值 - 歴史紀錄返回視窗 - 中繼次數 節點距 裝置 主機資訊 @@ -590,18 +498,12 @@ 我同意。 我已閱讀並理解上述內容。我同意透過 MQTT 傳輸未加密的節點資料 我知道我在做什麼。 - I2S 時鐘 - I2S 數據輸入 - I2S 數據輸出 - I2S WS 訊號選擇 室内空氣品質指標(IAQ) (室內空氣品質) 相對尺度 IAQ 值,由 Bosch BME680 測量。值範圍 0–500。 圖示說明 - 忽略 將 '%1$s' 加入忽略清單嗎? 忽略來訊 - 無視MQTT 從忽略清單中移除 '%1$s' 嗎? 匯入設定 @@ -621,7 +523,6 @@ IP IP地址: IP連接埠: - 第四代IP模式 JSON輸出已啟用 %1$s @@ -635,8 +536,6 @@ 緯度 瞭解詳情 - LED 心跳指示 - LED狀態 舊版管理頻道 %1$d 函式庫 @@ -696,11 +595,9 @@ 照度 管理自定義圖磚來源 管理地圖圖層 - 託管模式 需要手動定位位置 Mesh 地圖 - 快取容量: %1$d MB\n快取使用: %2$d MB 快取管理 目前快取大小 %1$d 圖磚 @@ -718,11 +615,8 @@ 離線管理 SQL快取清除失敗,請查看 logcat 以獲取詳細資訊。 清除 %1$s 的 SQL 快取 - 地圖報告 同意透過 MQTT 分享未加密的節點資料 啟用此功能即表示您認知並明確同意透過 MQTT 協議傳輸您裝置的即時地理位置,且不進行加密。此位置資料可能用於即時地圖回報、裝置追蹤及相關遙測功能等用途。 - 地圖報告間隔(秒) - 您的節點將定期發送未加密的地圖回報封包至已設定的 MQTT 伺服器,包含 ID、長名稱與短名稱、大約位置、硬體型號、角色、韌體版本、LoRa 區域、數據機預設值以及主要頻道名稱。 選擇下載地區 開始下載 地圖樣式選擇 @@ -768,9 +662,6 @@ 不明 訊息 最小 - 最短廣播間隔 (秒) - 智慧距離 - 智慧間隔 最小喚醒時間 預設值 模組設定 @@ -780,7 +671,6 @@ MQTT MQTT配置 - 啟用MQTT服務器 找不到伺服器 測試失敗 Broker 遭拒:%1$s @@ -812,14 +702,12 @@ 已靜音 %1$d 天 %2$s 小時 已靜音 %1$s 小時 未靜音 - 通知逾時時間(秒) 名稱 名稱不得空白。 返回上一頁 導航至 相鄰設備資訊 鄰居資訊配置 - 啟用鄰居資訊 網路 收到新的頻道 URL 下方有新的訊息 @@ -832,7 +720,6 @@ 未偵測到藍牙裝置 沒有自定義圖專來源。 未選擇裝置 - 找不到裝置 未發現任何檔案。 沒有可用的統計資料 未載入自訂圖層。 @@ -885,7 +772,6 @@ 通過喜好 有節點MQTT排序 重設節點資料庫 - 節點資訊廣播間隔 節點 位於此處的節點 @@ -900,6 +786,8 @@ 未連線 注意 備註 + + 網狀網路 (Mesh) Meshtastic 使用通知功能讓您隨時了解新訊息和其他重要事件。您可以隨時在設定中更新通知權限。 頻道訊息與私訊通知。 @@ -908,12 +796,8 @@ 警音/振鈴 回執通知 消息已讀通知 現在 - 時間伺服器 - 紀錄數目 - 允許轉發至 MQTT 好的 - OLED 類型 24 小時 1 小時 @@ -929,19 +813,9 @@ 開放原始碼函式庫 選項 定位朝北 - - 輸出蜂鳴(GPIO) - 輸出持續時間(毫秒) - 輸出LED 高電平觸發 - 輸出LED(GPIO) - 輸出振動(GPIO) 溢出選單 覆蓋控制台序列埠 - 覆蓋工作週期/佔空比 - 手動設定頻率 - 停用PA風扇 - 配對模式 密碼 PAX @@ -953,7 +827,6 @@ W:%1$d 客流量計數 人流計數(Paxcount)設置 - 已啟用人流計數(Paxcount) 週期性位置廣播 Meshtastic 應用程式需要啟用「鄰近裝置」權限,才能透過藍牙尋找並連接到裝置,可以選擇在不使用時停用。 @@ -971,20 +844,16 @@ %1$d 秒 + 位置 使用手機目前定位 位置已啟用 - 位置標誌 位置 定位封包 - 電源 電源設定 電源計量資料 - 啟用電池資訊模組 - 在螢幕上顯示電量資訊 - 電源資訊更新間隔 已供電 精確位置 語言 @@ -1005,12 +874,9 @@ 文字 主要 定期廣播位置與遙測資料 - 私鑰 將手機位置提供給Mesh網路 服務供應商名稱已存在。 - 啟用對客戶端的代理 PSK - PTT針腳 公鑰 公鑰已變更 QRCODE @@ -1028,25 +894,14 @@ 降雨(24h) 範圍測試 範圍測試設定 - 啟用範圍測試 回應 重新開機 - - 轉發模式 - 重播任何觀察到的訊息,如果它是在我們的私人頻道上或來自具有相同 lora 參數的其他網路上。 - 與「ALL」行為相同,但會跳過封包解碼,僅重新廣播它們。此功能僅適用於中繼器角色。在其他角色上設定此功能將導致「ALL」行為。 - 忽略來自非標準通訊埠號(諸如 TAK、RangeTest、PaxCounter 等)的封包,僅重新廣播標準通訊埠號的封包:NodeInfo、Text、Position、Telemetry 和 Routing。 - 近似於 LOCAL_ONLY 角色,將忽略來自外部 Mesh 節點的訊息,同時也忽略已知節點列表以外節點的訊息。 - 忽略來自開放的或無法解密的外部 Mesh 觀察到的訊息。僅轉播來自本地節點的主要/次要頻道的訊息。 - 僅允許 SENSOR、TRACKER 和 TAK_TRACKER 角色,與 CLIENT_MUTE 角色不同,此模式將禁止所有重新廣播行為。 最近的網路裝置 重新連接中… - 紅色 重新整理 重新整理中繼資料 您確定要重新產生密鑰嗎?\n\n連線過的其他節點需要刪除並重新交換金鑰後才能恢復加密通訊連線。 重新產生私鑰 - 地區 聽到 %1$d 個中繼 @@ -1094,31 +949,17 @@ 裝置角色 Client 客戶端基礎模式 - 將來自或發往我的最愛節點的封包視為 ROUTER_LATE,其他所有封包視為 CLIENT。 - 應用程式連接或獨立收發裝置。 Client Hidden - 基於省電或隱私需求,僅提供最低限度廣播通訊的節點。 Client Mute - 對其他裝置封包不予轉播的節點。 Lost and Found Repeater - 加強網路覆蓋的中繼基地台節點,但轉播時僅添加最低限度的額外負擔(Overhead)。不會顯示在節點列表上。 Router Router Client - 兼具路由器和用戶端功能的節點。行動裝置不宜使用。 - 加強網路覆蓋的中繼基地台節點。顯示在節點列表上。 Router Late - 基礎建設節點,總是在所有其他模式之後才重新廣播一次封包,以確保本地群集有額外的覆蓋範圍。在節點清單中可見。 Sensor - 優先廣播遙測資料封包。 TAK - 最佳化以供 ATAK 系統通訊使用,減少日常廣播量。 TAK Tracker - 啓用自動 TAK PLI 廣播,將減少定期廣播。 Repeater - 優先廣播 GPS 位置封包。 - 根話題 - 啟用旋轉編碼器#1 我已閱讀<a href="https://meshtastic.org/docs/configuration/radio/device/#roles">裝置角色說明文件</a>以及關於<a href="http://meshtastic.org/blog/choosing-the-right-device-role">選擇正確裝置角色</a>的部落格文章。 接收到拒絕確認 @@ -1127,12 +968,9 @@ 逾時 RSSI 接收信號強度指示(RSSI)用於測量天線所接收到信號的功率強度。 RSSI 值越高通常代表連線越強且穩定。 - rsyslog伺服器 衛星數 - 儲存 儲存 - 將 .CSV 保存到內部儲存空間(僅限ESP32) 匯出範圍測試封包 掃描 @@ -1144,7 +982,6 @@ 掃描聯絡人分享 QR Code 正在搜尋… 正在搜尋… - 螢幕開啟持續時間 移至最底部 搜尋表情符號…… 次要 @@ -1172,19 +1009,8 @@ 已選取 已選擇的地圖類型 傳送 - 發送振鈴 - 告警訊息發送提示音 - 訊息發送間隔(秒) - 序列埠 - 序列埠鮑率 序列埠設定 - 序列控制台 - 啟用序列埠 - 序列埠模式 - RX - TX - 伺服器 連線階段進行中 需要重新整理 設定連線 @@ -1208,7 +1034,6 @@ 顯示路徑 關機 裝置:%1$s - 電源中斷時關機 ⚠️ 這將會關閉節點。需要實體操作才能重新開啟。 信號 信號品質 @@ -1216,22 +1041,16 @@ 顯示器 跳過 時隙 - 智慧定位 SNR 信噪比(SNR),用於通訊中量化所需信號與背景噪音水平的指標。在 Meshtastic 及其他無線系統中,信噪比越高表示信號越清晰,可以提高數據傳輸的可靠性和品質。 土壤濕度 土壤溫度 速度 %1$d Km/h - 擴頻因子 - SSID - 狀態廣播間隔 (秒) 狀態訊息 隨時隨地保持連線 儲存 & 轉發 儲存 & 轉發設定 - 已啟用儲存 & 轉發 - 子網 成功 超深度睡眠時長 已支援 @@ -1239,21 +1058,10 @@ 刪除 靜音 解除靜默 - 接收增益提升 系統設定 TAK (ATAK) TAK 設定 - 隊員角色 - 前進觀測員 (FO) - 司令部 (HQ) - 汪星人 (K9) - 醫療兵 - 無線電兵 - 狙擊手 - 隊長 - 隊伍成員 - 未指定 TAK 伺服器 啓用本地 TAK 伺服器 … @@ -1264,22 +1072,6 @@ ✗ 執行 執行中:%1$s - 隊伍顏色 - Blue - 藍色 - 咖啡色 - 天青色 - 深藍色 - 墨綠色 - Green - 綠色 - 洋紅色 - 栗紅色 - 橙色 - 紫色 - Red - 紅色 - 羽青色 - 未指定 - 白色 - 黃色 遙測 遙測設定 溫度 @@ -1288,10 +1080,8 @@ 淺色 系統預設 時間 - 時區 逾時 時間戳記 - TLS已啟用 切換我的位置 路由追蹤 @@ -1334,7 +1124,6 @@ 不明封包閾值 翻譯 - 通過Lora無線電傳輸 低功耗藍牙 LoRa @@ -1345,11 +1134,11 @@ 二十四小時 48 小時 二週 - 啟用 LoRa 發射 - 發射功率 類別 請輸入訊息 UDP 廣播 + dBm + 公尺 系統預設 英制(英里/英尺) @@ -1367,9 +1156,6 @@ 取消靜音選取項目 無法識別 預設值 - 0 - 啟用上下選擇輸入 - GPS 輪詢間隔 - 更新間隔(秒) 已更新 來自網狀網路的訊息會經由任何配置閘道的節點傳送到網際網路。 運行時間 @@ -1379,13 +1165,7 @@ 網址必須包含佔位符。 URL 範本 USB - - 使用12小時制 使用同形異意字元編碼處理西里爾字母 - 使用 I2S 控制蜂鳴器 - 使用輸入上拉模式 - 使用預設值 - 使用PWM調製的蜂鳴 使用者 用戶規劃 @@ -1393,7 +1173,6 @@ 用戶資訊 使用者設定 使用者資訊 - 用戶名 紫外線強度 (UV Lux) 透過 API 有節點MQTT排序 @@ -1401,8 +1180,6 @@ 在地圖上檢視 查看版本資訊 電壓 - 藍牙等待持續時間 - 輕觸或移動喚醒 警告 刪除航點? 編輯航點 diff --git a/core/resources/src/commonMain/composeResources/values/schema_strings.xml b/core/resources/src/commonMain/composeResources/values/schema_strings.xml new file mode 100644 index 0000000000..82e8253534 --- /dev/null +++ b/core/resources/src/commonMain/composeResources/values/schema_strings.xml @@ -0,0 +1,546 @@ + + + + + Blue + The blue level of the ambient lighting LED. + Current + Drive current for the LED output. + Green + The green level of the ambient lighting LED. + LED State + The state of the LED (on/off) + Red + The red level of the ambient lighting LED. + 1200 bps + 1300 bps + 1400 bps + 1600 bps + 2400 bps + 3200 bps + 450 bps + 700C bps + Default + Bitrate + The Codec2 bitrate to use. The sample rate is always 8 kHz. Lower bitrates use less bandwidth but reduce audio quality. + Codec2 Enabled + Enable Codec2 audio encoding/decoding for voice communication over the mesh. + I2S DIN + I2S SCK + I2S SD + I2S WS + PTT Pin + Push-to-talk GPIO pin number. + Bluetooth Enabled + Enable Bluetooth on the device + Fixed Pin + Fixed PIN for Bluetooth pairing. Used when pairing mode is set to fixed PIN + Pairing Mode + Bluetooth pairing strategy + Fixed Pin + No PIN (Just Works) + Random Pin + Counter Clockwise Rotary Event + Input event for counter-clockwise rotation + Clockwise Rotary Event + Input event for clockwise rotation + Encoder Press Event + Input event for encoder press + Pin A + GPIO pin for rotary encoder A port. + Pin B + GPIO pin for rotary encoder B port. + Press Pin + GPIO pin for rotary encoder Press port. + Back + Cancel + Down + Left + None + Right + Select + Up + Rotary 1 + Enable rotary encoder + Send Bell + Send bell character with messages + Up Down 1 + Enable up/down/select input + TriggerType + Type of trigger event + Detection Sensor Enabled + Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart. + Minimum time between detection broadcasts + Minimum time between detection broadcasts. + GPIO Pin to monitor + GPIO pin watched for state changes. + Name + Sensor name for mesh messages + Send Bell + Send ASCII bell with alert message. Useful for triggering external notification on bell. + State Broadcast Interval + How often to send the detection sensor state to the mesh, whether or not anything was detected. + Either Edge High + Either Edge Low + Falling Edge + High + Low + Rising Edge + Uses pullup resistor + Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin + Button GPIO + GPIO pin for the user button, can be remapped on boards with multiple buttons + Buzzer GPIO + GPIO pin for the PWM buzzer + Disable Triple Click + Disables the user button triple-press shortcut. + Double Tap as Button + Treat double tap on supported accelerometers as a user button press. + LED Heartbeat + Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable. + Node Info Broadcast Interval + How often node information is sent. Defaults to 900 seconds. + Rebroadcast Mode + All + Rebroadcast any observed message, if it was on our private channel or from another channel with the same lora params. + All Skip Decoding + 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. + Core Portnums Only + Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing. + Known Only + 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. + Local Only + 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. + None + Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. + Device Role + Client + Client Base + Used for rooftop nodes to distribute messages more widely from multiple nearby client mute nodes. + App connected or stand alone messaging device. + Client Hidden + Device that only broadcasts as needed for stealth or power savings. + Client Mute + Device that does not forward packets from other devices. + Lost and Found + Broadcasts location as message to default channel regularly for to assist with device recovery. + Repeater + Deprecated infrastructure role that creates gaps in the mesh rebroadcast chain. Switch this node to a Router-based role (Router or Router Late). + Router + Router Client + Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list. + Router Late + Infrastructure node that always rebroadcasts packets once but only after all other modes. Visible in Nodes list. Not a good choice for rooftop nodes. + Sensor + Broadcasts telemetry packets as priority. + TAK + Optimized for ATAK system communication, reduces routine broadcasts. + TAK Tracker + Enables automatic TAK PLI broadcasts and reduces routine broadcasts. + Tracker + Broadcasts GPS position packets as priority. + Time Zone + POSIX timezone definition string + Carousel Interval + Automatically moves to the next screen page, like a carousel, on this interval. + Always point north + The compass heading on the screen outside of the circle will always point north. + Compass Orientation + Indicates how to rotate or invert the compass output for accurate display. + 0° + 0° Inverted + 180° + 180° Inverted + 270° + 270° Inverted + 90° + 90° Inverted + Display Mode + TFT Full Color Displays + Default 128x64 screen layout + Override default screen layout. + Inverted top bar for 2 Color display + Optimized for 2 color displays + Imperial + Metric + Flip Screen + Flip screen vertically + Bold Heading + Bold the heading text on the screen. + OLED Type + Override automatic OLED screen detection. + Detect Automatically + SH 1106 + SH 1107 + SH 1107 128x128 + SH 1107 Rotated + SSD 1306 + Screen on for + How long the screen remains on after the user button is pressed or messages are received. + Display Units + Units shown on the device screen. + 12 Hour Clock + Sets the screen clock format to 12-hour. + Wake Screen on tap or motion + Requires that there be an accelerometer on your device. + Active + If enabled, the 'output' Pin will be pulled active high, disabled means active low. + Alert when receiving a bell + Alert GPIO buzzer when receiving a bell + Buzz on bell character + Alert on bell character + Alert GPIO vibra motor when receiving a bell + Vibrate on bell character + Alert when receiving a message + Alert GPIO buzzer when receiving a message + Buzz on incoming message + Alert on incoming message + Vibra Motor Alert + Alert GPIO vibra motor when receiving a message + External Notification Enabled + Enable external notifications + Nag Timeout + How long the notification lasts. + Output pin GPIO + Output pin buzzer GPIO + Buzzer output pin + GPIO pin driven on notification. Defaults to the board's EXT_NOTIFY_OUT pin. + GPIO Output Duration + In GPIO mode, how long to keep the output on. + Output pin vibra GPIO + Vibration motor output pin + Use I2S As Buzzer + Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability. + Use PWM Buzzer + 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. + Bandwidth + Frequency Slot + Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name. + Coding Rate + Error-correction redundancy, as the denominator of 4/n. Higher values survive noisier links but make every packet longer. + Ok to MQTT + Hop Limit + How many times a message may be repeated before it stops being forwarded. + Ignore Incoming + Ignore MQTT + Ignore packets received over LoRa that travelled via MQTT anywhere on their path. + Presets + Lite - Fast + Lite - Slow + Long Range - Fast + Long Range - Moderate + Long Range - Slow + Long Range - Turbo + Medium Range - Fast + Medium Range - Slow + Medium Range - Turbo + Narrow - Fast + Narrow - Slow + Short Range - Fast + Short Range - Slow + Short Range - Turbo + Tiny - Fast + Tiny - Slow + Very Long Range - Slow + Override Duty Cycle + Frequency Override + PA Fan Disabled + Region + The region where you will be using your radios. + Australia / New Zealand + Australia / New Zealand 433MHz + Brazil 902MHz + China + European Union 433MHz + European Union 866MHz + European Union 868MHz + European Union 874MHz + European Union 917MHz + European Union 868MHz (Narrow) + India + ITU Region 1 / Amateur 2m + ITU Region 1 / Amateur 70cm + ITU Region 2 / Amateur 1.25m + ITU Region 2 / Amateur 2m + ITU Region 2 / Amateur 70cm + ITU Region 3 / Amateur 2m + ITU Region 3 / Amateur 70cm + Japan + Korea + Kazakhstan 433MHz + Kazakhstan 863MHz + 2.4 Ghz + Malaysia 433MHz + Malaysia 919MHz + Nepal 865MHz + New Zealand 865MHz + Philippines 433MHz + Philippines 868MHz + Philippines 915MHz + Russia + Singapore 923MHz + Thailand + Taiwan + Ukraine 433MHz + Ukraine 868MHz + Please set a region + United States + Spread Factor + Number of chirps per symbol, as 2 raised to this value. + RX Boosted Gain + Enable RX boosted gain mode on SX126X based radios + Transmit Enabled + Allow the LoRa radio to transmit. Turn off while hot-swapping antennas or bench testing. + Transmit Power + Radio transmit power. Leave at zero to use the highest level legal for the region, which is what most radios should use. + Use Preset + Use the modem preset settings instead of a manual bandwidth, spread factor and coding rate + Map Publish Interval + How often a map report is published. + Report Location + I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT. + Forward Observer + HQ + K9 + Medic + RTO + Sniper + Team Lead + Team Member + Default (Team Member) + Interval + How often a beacon is broadcast. + Message + Message for beacon broadcasts + Address + MQTT server address + MQTT Enabled + Enable MQTT gateway + Encryption Enabled + Send encrypted packets to MQTT + Map Reporting + 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. + Password + MQTT password + MQTT Client Proxy + Utilizes the network connection on your phone to connect to MQTT. + Root Topic + MQTT root topic + TLS Enabled + TLS is required for the public Meshtastic MQTT server. + Username + MQTT username + Neighbor Info Enabled + Enable neighbor info broadcasting. Periodically sends information about directly-heard neighbors to help visualize mesh topology. + Transmit over LoRa + Whether to transmit neighbor info over LoRa in addition to MQTT and PhoneAPI. Not available on channels with default key and name. + Update Interval + How often to broadcast neighbor info. + Address Mode + DHCP + Static + Enabled Protocols + Enable broadcasting packets via UDP over the local network. + Ethernet Enabled + Enabling Ethernet will disable the bluetooth connection to the app. + DNS + Gateway + IP + Subnet + NTP Server + NTP server address. Defaults to meshtastic.pool.ntp.org + None + UDP Broadcast + Rsyslog Server + WiFi Enabled + Enabling WiFi will disable the bluetooth connection to the app. + Password + WiFi password for authentication + SSID + WiFi network name to connect to + BLE Threshold + RSSI threshold for counting BLE devices. + PAX Counter Enabled + When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work. + Update Interval + How often we can send a message to the mesh when people are detected. + WiFi Threshold + RSSI threshold for counting WiFi devices. + Minimum Distance + The minimum change in distance before a smart position broadcast is considered. + Minimum Interval + The shortest interval between position updates once the minimum distance has been met. + Fixed Position + The last known latitude, longitude and altitude are broadcast over the mesh on the position interval, rather than a live GPS fix. + GPS EN GPIO + GPIO pin for GPS enable + GPS Mode + Update Interval + How often to try to get a GPS position. + Disabled + Enabled + Not Present + Broadcast Interval + The longest a node will go without broadcasting a position. + Smart Position + Position Flags + Optional fields to include when assembling position messages + Altitude + Include an altitude value in position reports, when one is available. + Altitude is Mean Sea Level + DOP + Include the dilution of precision value. PDOP is used by default. + Altitude Geoidal Separation + Vehicle heading + HDOP / VDOP + If DOP is set, send separate HDOP and VDOP values instead of PDOP. + Number of satellites + Sequence number + Vehicle speed + Timestamp + GPS Receive GPIO + GPIO pin for GPS RX + GPS Transmit GPIO + GPIO pin for GPS TX + ADC Override + Power Saving + Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. + Shutdown on Power Loss + How long after external power is removed before the device powers off. Zero to disable. + Wait for Bluetooth Duration + Range Test Enabled + Enable range test module + Save + Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server. + Sender Interval + This device will send out range test messages on the selected interval. + Admin Key + The public key authorized to send admin messages to this node + Debug Logs + Output live debug logging over serial, view and export position-redacted device logs over Bluetooth. + Managed Device + Device is managed by a mesh administrator, the user is unable to access any of the device settings. + Balanced - Prefer Authenticated + Prefer authenticated packets, but still accept unsigned traffic from nodes not known to sign. + Compatible - Accept Unsigned + Accept unsigned traffic for maximum compatibility. A signature that can be checked and is wrong still drops the packet. + Strict - Require Authentication + Accept only packets with a verified signature or successful PKI decryption. Packets from older nodes may be ignored. + Private Key + Used to create a shared key with a remote device + Public Key + Generated from your private key and sent out to other nodes on the mesh to allow them to compute a shared secret key + Serial Console + Serial Console over the Stream API. + Baud + Serial baud rate + Echo + If set, any packets you send will be echoed back to your device. + Serial Enabled + Enable serial module + Mode + Serial module operation mode + Receive data (rxd) GPIO pin + RX pin number + 110 Baud + 115200 Baud + 1200 Baud + 19200 Baud + 230400 Baud + 2400 Baud + 300 Baud + 38400 Baud + 460800 Baud + 4800 Baud + 57600 Baud + 576000 Baud + 600 Baud + 921600 Baud + 9600 Baud + Default + CALTOPO + Default + NMEA Positions + Protobufs + Simple + Text Message + Timeout + The amount of time to wait before we consider your packet as done. + Transmit data (txd) GPIO pin + TX pin number + Store and Forward Enabled + Enables the store and forward module. + Send Heartbeat + Send a heartbeat to advertise the server's presence. + History Return Max + History Return Window + Server + Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM. + Number of records + Role + TAK member role + Team + TAK team color + Blue + Brown + Cyan + Dark Blue + Dark Green + Green + Magenta + Maroon + Orange + Purple + Red + Teal + Default (Cyan) + White + Yellow + Air Quality Metrics Enabled + Collect air quality metrics + Air Quality Metrics Interval + How often air quality metrics are sent over the mesh. + Broadcast Device Metrics + Enable broadcasting device metrics to the mesh network. When disabled, metrics are only sent to connected clients. + Device Metrics Interval + How often device metrics are sent over the mesh. + Display Fahrenheit + Display environment in Fahrenheit + Environment Metrics Enabled + Collect environment measurements + Show on device screen + Display environment measurements on device + Environment Metrics Interval + How often environment metrics are sent over the mesh. + Power Measurement Enabled + Collect power metrics + Power Screen + Display power metrics on device + Power Metrics Interval + How often power metrics are sent over the mesh. + Direct NodeInfo Max Hops + Only answer requestors within this many hops. + Minimum Position Interval + Positions from the same node arriving sooner than this are dropped. + Rate Limit Max Packets + The most packets one node may send per window. + Rate Limit Window + The time window packets are counted over. + Unknown Packet Threshold + How many per window before the sender is dropped. + diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index ed20ff928d..83929a8dff 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -17,18 +17,24 @@ + Humidity %1$s: %2$s Message from %1$s: %2$s battery %1$d% + Channel %1$d %1$s away favorite - %1$d hops away + + %1$d hop away + %1$d hops away + last heard %1$s offline online role %1$s signal %1$s Hop %1$d: %2$d nodes + Temperature About Accept Acknowledgements @@ -46,7 +52,6 @@ Translate message Actions ADC multiplier override - ADC multiplier override ratio ADC Voltage Add @@ -60,7 +65,6 @@ Add device manually… Add Network Layer Address - Admin Key Admin Keys Administration Advanced @@ -70,24 +74,14 @@ Air Quality Air quality icon Air Quality Metrics Log - Air quality metrics module enabled - Air quality metrics update interval Percent of airtime for transmission used within the last hour. AirUtil - - Alert bell buzzer - Alert bell LED Alert Bell Character! - Alert bell vibra - Alert message buzzer - Alert message LED - Alert message vibra All Allow input source Allow undefined pin access Alt Altitude - Always point north Ambient Lighting Ambient Lighting Config Ammonium @@ -138,8 +132,6 @@ Saves the public and private keys to secure, encrypted storage on this device. Backup & Restore Bad - - Bandwidth Default (%1$s kHz) %1$s kHz Unsupported (%1$s) @@ -149,15 +141,12 @@ Battery INA_2XX I2C address BOD Bluetooth Devices - BLE RSSI threshold (defaults to -80) Bluetooth scanning also needs location services switched on for this version of Android. Your position is not used. - Blue Bluetooth Available Bluetooth Devices Bluetooth Config Bluetooth is off. Turn it on to scan for nearby devices. - Bluetooth enabled Configuration Wirelessly manage your device settings and channels. Discovery @@ -178,16 +167,12 @@ Bluetooth scan limit reached. Try again in %1$d second. Bluetooth scan limit reached. Try again in %1$d seconds. - Bold Heading Pairing failed. Grant nearby device permissions and try again. Pairing did not complete. Try pairing again. Nearby devices permission is off, so your radio cannot be reached over Bluetooth. Tap to turn it back on. Meshtastic can't reconnect Settings - Broadcast Interval Busy floor - Button GPIO - Buzzer GPIO Lowering this will permanently delete the saved history for %1$d device. Lowering this will permanently delete the saved history for %1$d devices. @@ -205,7 +190,6 @@ Canned message enabled Couldn't change channel, because radio is not yet connected. Please try again. Shutdown not supported on this device - Carousel interval Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). Ch @@ -219,7 +203,6 @@ Channel 8 Channel Features This Channel URL is invalid and can not be used - Channel Channel Name Channel URL ChUtil @@ -238,6 +221,7 @@ Oops! We hit some heavy packet loss or signal interference! 📡 Let's re-transmit that query and see if we can get through. I'm operating on a limited frequency on this app flavor! 🔌 Try using a version with full AI Core integration. Oh no! My antenna can't seem to establish a connection on this platform. 📡 Please double-check your hardware compatibility! + AI assistant is not available on this platform. Here are pages that may help: Hey there! 📡 I'm Chirpy, your on-device Meshtastic assistant! I can help with setup, configuration, troubleshooting, and mesh networking tips. What can I help you with? Ask Chirpy Ask about Meshtastic… @@ -265,9 +249,6 @@ CO₂ CO₂ Hum CO₂ Temp - CODEC 2 enabled - CODEC2 sample rate - Coding Rate Collapse chart Collapsed Communicate off-the-grid with your friends and community without cell service. @@ -280,49 +261,28 @@ Location permission is required to show distance and bearing. This device does not have a compass sensor. Heading is unavailable. Compass north top - Compass orientation Compass Estimated area: \u00b1%1$s (\u00b1%2$s) Estimated area: unknown accuracy Compromised keys detected, select OK to regenerate. - Treat double tap on supported accelerometers as a user button press. Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDs, the charger and GPS LEDs are not controllable. - Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. Not available on a channel with default key and name. Send a position on the primary channel when the user button is triple clicked. Time zone for dates on the device screen and log. Use phone time zone - Automatically toggles to the next page on the screen like a carousel, based the specified interval. - The compass heading on the screen outside of the circle will always point north. - Override default screen layout. - Flip screen vertically. - Bold the heading text on the screen. - Override automatic OLED screen detection. - How long the screen remains on after the user button is pressed or messages are received. - Units displayed on the device screen. - Requires that there be an accelerometer on your device. - Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name and will change from the default public slot. Change back to the public default slot if private primary and public secondary channels are configured. - Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs. + 4/%1$d + Coding Rate Override + This preset already uses the highest coding rate. + Adds error correction on top of the preset. A higher coding rate makes every packet longer on air and uses more of the duty cycle and channel utilization budget. + Preset default (%1$s) This region's presets are for licensed (amateur radio) operators only. Enable Licensed amateur radio (Ham) in User Config to select them. Available modem presets, default is Long Fast. The region where you will be using your radios. - Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices. Enable broadcasting packets via UDP over the local network. - Enabling Wi-Fi will disable the bluetooth connection to the app. - The maximum interval that can elapse without a node broadcasting a position. - The minimum distance change in meters to be considered for a smart position broadcast. - The fastest that position updates will be sent if the minimum distance has been satisfied. - Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss. - How often should we try to get a GPS position (<10sec keeps GPS on). - Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. The node is restarting and will be briefly unreachable. - The public key authorized to send admin messages to this node. - Output live debug logging over serial, view and export position-redacted device logs over Bluetooth. - Device is managed by a mesh administrator, the user is unable to access any of the device settings. Used to create a shared key with a remote device. The device does not share its private key over remote administration. You can set a new key, but it can never be read back. Generated from your private key and sent out to other nodes on the mesh to allow them to compute a shared secret key. - Serial Console over the Stream API. Configuration Configure Bluetooth Permissions Configure Critical Alerts @@ -375,7 +335,6 @@ Filter included Preset Filters Filters - Debug log API enabled No app logs to show Refresh Export Logs @@ -426,8 +385,6 @@ Details Detection Sensor Detection Sensor Config - Detection Sensor enabled - Detection trigger type Device Device configuration @@ -442,17 +399,15 @@ Device Metrics %1$s %1$s: %2$s%% - Device metrics update interval %1$s: %2$s V Device sleeping Device Storage & UI (Read-Only) - Send Device Telemetry - Enable/Disable the device telemetry module to send metrics to the mesh. These are nominal values. Congested meshes will automatically scale to longer intervals based on number of online nodes. Theme: %1$s, Language: %2$s Dew Point Direct Message Direct Message Key Direct Messages + Disabled Discard Disconnect Disconnected @@ -468,6 +423,7 @@ Dwell Time Time to listen on each preset No discovery sessions yet + Start a scan from Local Mesh Discovery to see its results here. Export report Discovery History Restored radio to home config (%1$s) after an interrupted discovery scan @@ -530,15 +486,14 @@ Stop Scan AI analysis not available %1$s remaining - %1$d unique nodes + + %1$d unique node + %1$d unique nodes + View map Disk Free %1$d - Display Device Display - Display mode - When enabled, the device will display the time in 12-hour format on screen. - Display units Dissolved O₂ Distance @@ -546,9 +501,10 @@ Filter the node list and mesh map based on proximity to your phone. Distance Measurements Display the distance between your phone and other Meshtastic nodes with positions. - DNS + Auto-translated Clear search + Community translated system ai,gemini,assistant,functions,automation,voice bluetooth,usb,tcp,pairing,serial,wifi debug,logs,logcat,export,bug report,issue,troubleshooting,diagnostics @@ -562,6 +518,7 @@ mqtt,broker,internet,bridge,uplink,downlink metrics,telemetry,signal,snr,rssi,battery,traceroute node,mesh,list,role,status,favorite,filter + notification,alert,sound,mute,reply,watch,wear os setup,welcome,permissions,first-launch module,serial,telemetry,canned,store-forward,administration settings,radio,lora,region,modem,device,power,security @@ -572,8 +529,12 @@ units,locale,metric,imperial,temperature,distance widget,home screen,local stats,glance,battery,utilization Loading documentation… + No content available. No documentation available No results found + Open %1$s + Page not found: %1$s + This page may have been moved or removed. Search documentation… Developer Guide User Guide @@ -590,6 +551,7 @@ MQTT Node Metrics Nodes + Notifications Getting Started Settings — Modules & Admin Settings — Radio & User @@ -602,15 +564,19 @@ Documentation Done Don't show again for this device - Double Tap as Button MQTT Downlink Enabled Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device. Download Download this area Duplicate Public Key Detected + + %1$s ago + %1$dd + %1$dh + %1$dm + %1$ds Dynamic Easily set up private mesh networks for secure and reliable communication in remote areas. - Echo enabled Edit Edit Network Tile Source 8 Hours @@ -629,28 +595,19 @@ Unable to load emoji No emoji found Recently Used - Enable power saving mode Enabled - - Encryption enabled Public key mismatch The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action. Public Key Encryption A public key is on file for this node, so interactions with it use public key encryption. Environment Metrics - Environment - Environment metrics module enabled - Environment metrics on-screen enabled - Environment metrics update interval - Environment metrics use Fahrenheit Error Duty Cycle limit reached. Cannot send messages right now, please try again later. Could not establish a stable connection after repeated attempts. Please re-select the node to retry. Connect & administer Establishing remote session… Ethernet Options - Ethernet enabled Ethernet IP: Running event firmware Use event theme @@ -666,7 +623,7 @@ Export TAK Data Package External Notification External Notification Config - External notification enabled + PWR Factory reset Fair Meshtastic %1$s @@ -677,10 +634,10 @@ Files available (%1$d): Add word or regex:pattern - Disable filtering - Enable Filtering - Enable filtering - Hide messages containing filter words + Disable filter here + Filter all conversations + Enable filter here + Hide messages containing filter words in every channel and DM. Turn it off for one conversation from its menu. Hide %1$d filtered Filter Filtered @@ -697,6 +654,11 @@ Firmware Edition %1$s has ended. Return to standard Meshtastic firmware to restore normal features. Update firmware + Latest: %1$s + Installed: %1$s + Latest version: %1$s. The installed version is shown once the device restarts into update mode. + Bootloader is up to date + Nothing to upgrade. The firmware is reinstalled next, which restarts the device. The erase file was copied but the device didn't start erasing. Nothing has been changed yet. Unplug the device, double-press its reset button, and try again. Couldn't copy the file to the device's drive. Make sure the drive is still connected and try again. The erase image list isn't available right now. Check your connection and try again, or use the web flasher at flasher.meshtastic.org. @@ -791,6 +753,8 @@ This might take a minute... Target: %1$s Firmware Update + %1$d% + %1$d% (%2$s/s, ETA: %3$ds) Unknown error Unknown hardware model: %1$d Unknown remote release @@ -811,9 +775,7 @@ Erase device during update Wipes the device's flash completely, then installs the selected firmware from scratch. Firmware version: %1$s - Fixed PIN Fixed Position - Flip screen For more information, see our privacy policy. Bold @@ -825,13 +787,7 @@ Free Memory Available system memory in bytes Freq - Frequency Slot - Friendly name Gas Resistance - Gateway - Generate input event on CCW - Generate input event on CW - Generate input event on Press Generate QR Code Geofence @@ -856,32 +812,18 @@ Get started GitHub Repository Good - GPIO GPIO pin - GPIO pin for rotary encoder A port - GPIO pin for rotary encoder B port - GPIO pin for rotary encoder Press port - GPIO pin to monitor - GPS EN GPIO - GPS Mode (Physical Hardware) - GPS Receive GPIO - GPS Transmit GPIO Grant permission - Green Optional. Appended to your call sign, e.g. KD2ABC//Attic Heltec Hardware Hardware model Heading - Heartbeat Help & Documentation Hide Layer Hide password - History return max - History return window No nodes heard in this window Nodes per Hop - Number of Hops Hops Away Host Host Metrics @@ -889,18 +831,13 @@ I agree. I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT I know what I'm doing. - I2S clock - I2S data in - I2S data out - I2S word select IAQ (Indoor Air Quality) relative scale IAQ value as measured by Bosch BME680. Value Range 0–500. + IAQ %1$d Icon Meanings - Ignore Add '%1$s' to ignore list? Ignore incoming - Ignore MQTT Remove '%1$s' from ignore list? Import configuration @@ -928,7 +865,6 @@ IP IP Address: Port: - IPv4 mode JSON output enabled %1$s %1$s +%2$d @@ -941,6 +877,7 @@ Key Verification Complete Key Verification Request Key Verification + %1$s (%2$s) Filter by Last Heard time: %1$s Last position update Latest alpha @@ -953,8 +890,6 @@ KML Network layer Learn more - LED Heartbeat - LED state Legacy Admin channel %1$d libraries License @@ -1004,6 +939,8 @@ Location permission is off. Turn it on in app settings to share your position. Without location access, the map will not show where you are and Meshtastic cannot share your position with the mesh. Everything else keeps working, and you can turn it on later. Meshtastic uses your location to place you on the map, measure distance to nodes, and — only if you turn it on — share your position with the mesh. Declining leaves everything else working. + Precise location is off. Turn on Use precise location in app settings to share your position. + Sharing your position with the mesh needs precise location, so other nodes see where you are rather than a rough area. When Android asks, choose Precise. The map and everything else keep working with approximate location. Location Sharing Try again in %1$d seconds. @@ -1053,6 +990,8 @@ Longitude LoRa LoRa + %1$s: %2$s → %3$s + LoRa Configuration Changes: Low Battery Node %1$s has a low battery (%2$d%) Low battery: %1$s @@ -1060,13 +999,10 @@ Lux Manage Custom Tile Sources Manage Map Layers - Managed Mode Manual position request required Mesh Map - Cache Capacity: %1$d MB\nCache Usage: %2$d MB Cache Manager - %1$s MB Current Cache size %1$d tiles Clear Downloaded Tiles @@ -1087,6 +1023,8 @@ Filter map Map layers support .kml, .kmz, or GeoJSON formats. Opacity: %1$d% + Couldn't open that map file. + Map files over %1$d MB can't be imported. %1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s Offline — showing cached map data No offline map data available right now — try again later @@ -1109,11 +1047,8 @@ Weather radar SQL Cache purge failed, see logcat for details SQL Cache purged for %1$s - Map reporting Consent to Share Unencrypted Node Data via MQTT 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. - Map reporting interval (seconds) - Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name. Select download region Start Download Map style selection @@ -1144,7 +1079,6 @@ Mesh invitations Listen for beacons Capture invitations advertised by nearby meshes - Beacon message Maximum %1$d bytes No channels available A nearby mesh invited you to join @@ -1176,15 +1110,30 @@ Meshtastic Alert notifications + Critical alerts sent by nodes on the mesh. Meshtastic Broadcast message notifications + Messages sent to your channels. + Radio notifications + Notices from your radio, such as key verification requests and security warnings. + Update and connection notifications + Firmware updates for your radio, and problems reconnecting to it. Low battery notifications + Your radio's battery is running low. Low battery notifications (favorite nodes) + A favorite node's battery is running low. Mesh invitation notifications + Invitations to join a nearby mesh. Direct message notifications + Messages sent directly to you. New node notifications + Nodes heard for the first time. + Reaction notifications + Emoji reactions to messages. Service notifications + Shows the connection to your radio while Meshtastic runs in the background. Waypoint notifications + Waypoints shared on the mesh, and geofence crossings. Message Message delivery status @@ -1221,10 +1170,12 @@ No acknowledgment was received in time. Try again when you have better signal or more mesh coverage. Message is too large to send Shorten the message and send it again. + Delivery proof failed Delivered to mesh Sending... Queued for sending Delivered to recipient + Delivered to recipient · proven Relayed, not confirmed by recipient Confirmed on SF++ chain Routing via SF++ chain… @@ -1234,9 +1185,6 @@ %1$s %2$d µg/m³ Min - Minimum broadcast (seconds) - Smart Distance - Smart Interval Minimum wake time Mirroring @@ -1265,7 +1213,6 @@ MQTT MQTT Config - MQTT enabled MQTT: connection lost MQTT: connection rejected (check credentials) MQTT proxy failed: %1$s @@ -1279,8 +1226,8 @@ Cannot reach broker (TCP) Timed out after %1$d ms TLS handshake failed: %1$s - MQTT proxy on this phone - This phone relays MQTT traffic for the connected device. Turn off to cut the relay immediately without changing the device's MQTT setting — useful when MQTT traffic is overwhelming the connection. Turn back on to resume relaying. + MQTT proxy in this app + This app relays MQTT traffic for the connected device. Turn off to cut the relay immediately without changing the device's MQTT setting, which helps when MQTT traffic is overwhelming the connection. Turn back on to resume relaying. Connected Connecting… Disconnected @@ -1288,6 +1235,8 @@ Inactive Reconnecting… Reconnecting (attempt %1$d) — %2$s + Connected, but the broker refused every topic: %1$s + Connected, but the broker refused %1$s Test connection You must set a region! You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our docs on this topic. @@ -1303,7 +1252,6 @@ Muted for %1$d days, %2$s hours Muted for %1$s hours Not muted - Nag timeout (seconds) Name Name cannot be empty. Navigate Back @@ -1313,7 +1261,6 @@ Meshtastic requires a compatible device. Our backers and partners offer ready-to-use hardware. Here are some of the most popular options. Neighbor Info Neighbor Info Config - Neighbor Info enabled Network https://example.com/map.kml or .geojson New Channel URL received @@ -1329,7 +1276,6 @@ No Bluetooth devices seen No custom tile sources found. No device selected - No devices found No files manifested. No Stats Available No map layers loaded. @@ -1402,7 +1348,6 @@ via Favorite via MQTT NodeDB reset - Node Info Broadcast Interval Nodes Nodes at this location @@ -1424,10 +1369,13 @@ Not now Note Notes + + Mesh Notifications are turned off and Android will not ask again. Turn them on in app settings to hear about new messages and alerts. Without notifications, Meshtastic cannot alert you to new messages, new nodes, or a low battery while the app is in the background. Notifications are how Meshtastic reaches you when the app is not open: new messages, newly discovered nodes, and a radio running low on battery. Nothing else changes if you decline. Meshtastic uses notifications to keep you updated on new messages and other important events. You can update your notification permissions at any time from settings. + %1$s to “%2$s” Notifications for channel and direct messages. Notifications for low battery alerts for the connected device. @@ -1435,8 +1383,6 @@ Notifications on alert/bell receipt Notifications on message receipt Now - NTP server - Number of records Offline maps No areas downloaded yet @@ -1444,9 +1390,7 @@ No terrain downloaded yet Offline Terrain Includes high-detail regional terrain - Ok to MQTT OK - OLED type 24 Hours 1 Hour @@ -1466,23 +1410,14 @@ Options Orient north ORP - - Output buzzer (GPIO) - Output duration (milliseconds) - Output LED active high - Output LED (GPIO) - Output vibra (GPIO) Overflow menu Override console serial port - Override Duty Cycle - Frequency Override - PA fan disabled Packet authenticity Balanced — Prefer authenticated Recommended. Reject unsigned downgrade attempts from nodes known to sign. Compatible — Accept unsigned - Authenticate packets when possible, but accept unsigned traffic for maximum compatibility. + Accept unsigned traffic for maximum compatibility. A signature that can be checked and is wrong still drops the packet. Protection level Strict — Require authentication Enable Strict @@ -1490,7 +1425,6 @@ Only show and process cryptographically authenticated mesh packets. Older nodes and oversized packets may disappear. Enable Strict authentication? This connected device does not support packet signature verification. - Pairing mode Password PAX @@ -1503,7 +1437,6 @@ Wi-Fi: %1$s Paxcounter Paxcounter Config - Paxcounter enabled Periodic position broadcast Scan channel and contact QR codes @@ -1515,6 +1448,7 @@ Find and connect to your radio over Bluetooth Alert you to messages, new nodes and low battery Allowed + Approximate only. Sharing your position with the mesh needs precise location. Blocked — tap to open system settings Denied — tap to allow Not required on this version of Android @@ -1538,6 +1472,10 @@ %1$d hour %1$d hours + + %1$d millisecond + %1$d milliseconds + %1$d minute %1$d minutes @@ -1563,17 +1501,12 @@ Position Set from current phone location Position enabled - Position Flags Position Position Packet Potassium - Power Power Config Power Metrics - Power metrics module enabled - Power metrics on-screen enabled - Power metrics update interval Powered ppm Precise location @@ -1596,14 +1529,11 @@ Primary Periodic position and telemetry broadcast https://meshtastic.org/docs/legal/privacy/ - Private Key Project information Provide phone location to mesh Provider name exists. - Proxy to client enabled PSK Português do Brasil - PTT pin Public Key Public Key Changed QR code @@ -1621,26 +1551,17 @@ Rain (24h) Range Test Range Test Config - Range test enabled React Reboot - - Rebroadcast Mode - Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters. - 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. - Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. - 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. - 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. - Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. + Reboot into DFU mode + The node stays in its bootloader and off the mesh until new firmware is flashed or it is reset. Recent Network Devices Reconnecting… - Red ***[REDACTED]*** Refresh Refresh metadata Are you sure you want to regenerate your Private Key?\n\nNodes that may have previously exchanged keys with this node will need to Remove that node and re-exchange keys in order to resume secure communication. Regenerate Private Key - Region Heard %1$d relay Heard %1$d relays @@ -1692,32 +1613,17 @@ Device Role Client Client Base - Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT. - App connected or standalone messaging device. Client Hidden - Device that only broadcasts as needed for stealth or power savings. Client Mute - Device that does not forward packets from other devices. Lost and Found - Broadcasts location as message to default channel regularly to assist with device recovery. Repeater - Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list. Router Router Client - Combination of both ROUTER and CLIENT. Not for mobile devices. - Infrastructure node for extending network coverage by relaying messages. Visible in nodes list. Router Late - Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in nodes list. Sensor - Broadcasts telemetry packets as priority. TAK - Optimized for ATAK system communication, reduces routine broadcasts. TAK Tracker - Enables automatic TAK PLI broadcasts and reduces routine broadcasts. Tracker - Broadcasts GPS position packets as priority. - Root topic - Rotary encoder #1 enabled I have read the <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Device Role Documentation</a> and the blog post about <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choosing The Right Device Role</a>. Admin session expired @@ -1740,15 +1646,12 @@ Message is too large to send RSSI Received Signal Strength Indicator, a measurement used to determine the power level being received by the antenna. A higher RSSI value generally indicates a stronger and more stable connection. - rsyslog server Salinity hey I found the cache, it is over here next to the big tiger. I'm kinda scared. Sats - Save Save & restart Save - Save .CSV in storage (ESP32 only) Export rangetest packets Scan @@ -1762,7 +1665,6 @@ Scan Shared Contact QR Code Scanning… Scanning… - Screen on for Scroll to bottom Search emoji... Search messages… @@ -1771,6 +1673,12 @@ No periodic telemetry broadcast Security + Delivery proof failed + Something acknowledged this message without the key only the recipient holds. + Delivery proof unchecked + A delivery proof arrived, but there was no key to check it against. + Delivery proven + The node you addressed proved it received this message. Warning Badge Security Status Dismiss @@ -1801,25 +1709,16 @@ Selected Selected Map Type Send - Send bell - Send bell with alert message - Sender message interval (seconds) - Serial - Serial baud rate Serial Config - Serial console - Serial enabled - Serial mode - RX - TX - Server Session active Refresh required Set time Set up connection Set your region settings + No settings match that + Search settings Share Share link @@ -1853,7 +1752,6 @@ Show Waypoints Shutdown Node: %1$s - Shutdown on power loss ⚠️ This will SHUTDOWN the node. Physical interaction will be required to turn it back on. Signal Signal Quality @@ -1887,7 +1785,6 @@ The system WebView is updating. Please try again in a moment. Skip Slot - Smart Position SNR Signal-to-Noise Ratio, a measure used in communications to quantify the level of a desired signal to the level of background noise. In Meshtastic and other wireless systems, a higher SNR indicates a clearer signal that can enhance the reliability and quality of data transmission. Soil Moist @@ -1898,39 +1795,24 @@ Speed %1$d Km/h %1$d mph - Spread Factor - SSID - State broadcast (seconds) Status Message A public status message that is broadcast to the mesh on change and every 12 hours. Stay Connected Anywhere Stop Connecting Store & Forward Store & Forward Config - Store & Forward enabled - Subnet Success Super deep sleep duration Supported Supported by Meshtastic Community + Independent maker hardware Delete Mute Unmute - RX Boosted Gain System Settings TAK (ATAK) TAK Configuration - Member Role - Forward Observer - Headquarters - Doggo (K9) - Medic - Radio Telephone Operator - Sniper - Team Lead - Team Member - Unspecified TAK Server TAK Mesh Channel Meshtastic channel used for outgoing TAK traffic @@ -1965,22 +1847,6 @@ Run Running: %1$s Connected node firmware doesn't support full TAK integration — only location and chat messages will bridge to ATAK. Markers and other event types need firmware 2.8.0 or newer. - Team Color - Blue - Brown - Cyan - Dark Blue - Dark Green - Green - Magenta - Maroon - Orange - Purple - Red - Teal - Unspecified - White - Yellow Telemetry Telemetry Config Temp @@ -1989,10 +1855,9 @@ Light System default Time - Time Zone Timeout Timestamp - TLS enabled + When this app relays MQTT it always connects to mqtt.meshtastic.org over TLS. This setting applies when the radio reaches the broker over its own Wi-Fi or Ethernet. Toggle my position Trace Route @@ -2044,7 +1909,6 @@ Message is already in your language Transmit is disabled This device can receive but will not send anything over LoRa. - Transmit over LoRa Transport API @@ -2059,12 +1923,13 @@ 24H 48 Hours 2W - Transmit Enabled - Transmit Power Type Type a message UDP broadcasting Undo + dBm + kHz + m Units System default @@ -2085,9 +1950,7 @@ Unpin Unrecognized Unset - 0 - Up/Down/Select input enabled - GPS Polling Interval - Update interval (seconds) + This device isn't supported Update status Updated MQTT Uplink Enabled @@ -2096,19 +1959,14 @@ URL URL cannot be empty. + Use https:// here. Plain http:// only works for localhost. URL must start with http:// or https://. URL must contain placeholders. URL Template https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png USB USB permission denied. Reconnect the device to try again. - - Use 12h clock format Compact encoding for Cyrillic - Use I2S as buzzer - Use INPUT_PULLUP mode - Use Preset - Use PWM buzzer User User Config @@ -2116,7 +1974,6 @@ User info User String User Info - Username UV Lux via API via MQTT @@ -2124,8 +1981,6 @@ View on map View Release Voltage - Wait for Bluetooth duration - Wake on tap or motion Warning Water pH Delete waypoint? @@ -2139,7 +1994,6 @@ Wi-Fi Options Wi-Fi Provisioning for mPWRD-OS - Wi-Fi enabled Wi-Fi IP: Available Networks Could not connect: %1$s @@ -2177,7 +2031,6 @@ Wi-Fi Provisioning for mPWRD-OS Invalid Wi-Fi Credential QR code format Scan Wi-Fi QR code - Wi-Fi RSSI threshold (defaults to -80) Not connected to Wi-Fi. Network scan may not find nearby devices. Wind diff --git a/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/DurationText.kt b/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/DurationText.kt new file mode 100644 index 0000000000..400245d9ca --- /dev/null +++ b/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/DurationText.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.resources + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.common.util.DurationUnitLabels +import kotlin.time.DurationUnit +import org.meshtastic.core.common.util.formatDuration as formatDurationWith + +/** [totalSeconds] as translated duration text, e.g. `1d 2h 3m`; see [formatDurationWith] for [smallest]. */ +@Composable +fun formatDuration(totalSeconds: Long, smallest: DurationUnit = DurationUnit.SECONDS): String { + // Loaded without arguments on purpose: each template keeps its %1$d for formatDurationWith to fill. + val labels = + DurationUnitLabels( + days = stringResource(Res.string.duration_days_short), + hours = stringResource(Res.string.duration_hours_short), + minutes = stringResource(Res.string.duration_minutes_short), + seconds = stringResource(Res.string.duration_seconds_short), + ) + return formatDurationWith(totalSeconds, labels, smallest) +} + +/** The suspending counterpart of [formatDuration], for text built outside composition such as notifications. */ +suspend fun formatDurationSuspend(totalSeconds: Long, smallest: DurationUnit = DurationUnit.SECONDS): String { + val labels = + DurationUnitLabels( + days = getStringSuspend(Res.string.duration_days_short), + hours = getStringSuspend(Res.string.duration_hours_short), + minutes = getStringSuspend(Res.string.duration_minutes_short), + seconds = getStringSuspend(Res.string.duration_seconds_short), + ) + return formatDurationWith(totalSeconds, labels, smallest) +} diff --git a/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/GetString.kt b/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/GetString.kt index a186ea313b..71dfddf746 100644 --- a/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/GetString.kt +++ b/core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/GetString.kt @@ -22,10 +22,13 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getPluralString as composeGetPluralString import org.jetbrains.compose.resources.getString as composeGetString -/** Retrieves a string from the [StringResource] in a blocking manner. Use primarily in non-composable code. */ +/** + * Retrieves a string from the [StringResource], blocking the calling thread on the resource loader. Only for callbacks + * that cannot suspend, such as a Glance `onCompositionError`; everything else uses [getStringSuspend]. + */ fun getString(stringResource: StringResource): String = runBlocking { composeGetString(stringResource) } -/** Retrieves a formatted string from the [StringResource] in a blocking manner. */ +/** Retrieves a formatted string from the [StringResource], blocking. Same restriction as the overload above. */ fun getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking { val resolvedArgs = formatArgs diff --git a/core/service/README.md b/core/service/README.md index 9e00c9a06b..eadeff3096 100644 --- a/core/service/README.md +++ b/core/service/README.md @@ -27,16 +27,15 @@ The in-process `RadioController` composition root (Desktop, iOS, and single-proc ```mermaid graph TB :core:service[service]:::kmp-library + :core:service -.-> :core:prefs :core:service --> :core:repository :core:service -.-> :core:common - :core:service -.-> :core:data :core:service -.-> :core:database :core:service -.-> :core:di :core:service -.-> :core:model :core:service -.-> :core:navigation :core:service -.-> :core:network :core:service -.-> :core:ble - :core:service -.-> :core:prefs :core:service -.-> :core:resources :core:service -.-> :core:takserver :core:service -.-> :core:testing diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts index 50dbe35556..9504265ece 100644 --- a/core/service/build.gradle.kts +++ b/core/service/build.gradle.kts @@ -27,14 +27,12 @@ kotlin { commonMain.dependencies { api(projects.core.repository) implementation(projects.core.common) - implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.navigation) implementation(projects.core.network) implementation(projects.core.ble) - implementation(projects.core.prefs) implementation(projects.core.resources) implementation(libs.meshtastic.protobufs) implementation(projects.core.takserver) @@ -55,6 +53,7 @@ kotlin { getByName("androidHostTest") { dependencies { + implementation(projects.core.prefs) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.work.testing) } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/app/MainActivity.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/app/MainActivity.kt index f7e5e38ce7..a5bc0e5126 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/app/MainActivity.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/app/MainActivity.kt @@ -19,7 +19,7 @@ package org.meshtastic.app import android.app.Activity /** - * Test-only stub for the real `MainActivity` in the `:androidApp` module. `AndroidNotificationManager` resolves the + * Test-only stub for the real `MainActivity` in the `:androidApp` module. `MeshNotificationManagerImpl` resolves the * activity by FQN via `Class.forName(...)` to avoid pulling `:androidApp` into `:core:service` as a Gradle dependency. * This stub lets unit tests exercise the deep-link `PendingIntent` construction path without that dependency. */ diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt deleted file mode 100644 index e221b90897..0000000000 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt +++ /dev/null @@ -1,414 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.notificationId -import org.meshtastic.proto.ClientNotification -import org.meshtastic.proto.DuplicatedPublicKey -import org.meshtastic.proto.KeyVerificationFinal -import org.meshtastic.proto.KeyVerificationNumberInform -import org.meshtastic.proto.KeyVerificationNumberRequest -import org.meshtastic.proto.LogRecord -import org.meshtastic.proto.LowEntropyKey -import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.Config -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -@RunWith(AndroidJUnit4::class) -@Config(sdk = [34]) -class AndroidNotificationManagerTest { - - private lateinit var context: Context - private lateinit var systemNotificationManager: NotificationManager - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - systemNotificationManager = context.getSystemService(NotificationManager::class.java)!! - clearManagedChannels() - systemNotificationManager.cancelAll() - } - - @After - fun tearDown() { - clearManagedChannels() - systemNotificationManager.cancelAll() - } - - @Test - fun `removeLegacyCategoryChannels deletes legacy channels and keeps canonical channels`() { - createChannel("NodeEvent") - createChannel(NotificationChannels.NEW_NODES) - - systemNotificationManager.removeLegacyCategoryChannels() - - assertNull(systemNotificationManager.getNotificationChannel("NodeEvent")) - assertNotNull(systemNotificationManager.getNotificationChannel(NotificationChannels.NEW_NODES)) - } - - @Test - fun `dispatch removes legacy node channel and creates canonical node channel`() = runTest { - createChannel("NodeEvent") - - val manager = AndroidNotificationManager(context) - manager.dispatch(Notification(title = "Node", message = "Seen", category = Notification.Category.NodeEvent)) - - assertNull(systemNotificationManager.getNotificationChannel("NodeEvent")) - assertNotNull(systemNotificationManager.getNotificationChannel(NotificationChannels.NEW_NODES)) - } - - @Test - fun `dispatch routes node event notifications to canonical new nodes channel`() = runTest { - val manager = AndroidNotificationManager(context) - - manager.dispatch(Notification(title = "Node", message = "Seen", category = Notification.Category.NodeEvent)) - - val posted = shadowOf(systemNotificationManager).allNotifications.last() - assertEquals(NotificationChannels.NEW_NODES, posted.channelId) - } - - @Test - fun `dispatch reports false when its notification channel is disabled`() = runTest { - createChannel(NotificationChannels.NEW_NODES, NotificationManager.IMPORTANCE_NONE) - val manager = AndroidNotificationManager(context) - - val dispatched = - manager.dispatch(Notification(title = "Node", message = "Seen", category = Notification.Category.NodeEvent)) - - assertFalse(dispatched) - assertEquals(0, shadowOf(systemNotificationManager).allNotifications.size) - } - - @Test - fun `removeLegacyCategoryChannels removes all known legacy category channels`() { - NotificationChannels.LEGACY_CATEGORY_IDS.forEach(::createChannel) - - systemNotificationManager.removeLegacyCategoryChannels() - - NotificationChannels.LEGACY_CATEGORY_IDS.forEach { legacyId -> - assertNull(systemNotificationManager.getNotificationChannel(legacyId)) - } - } - - @Test - fun `removeLegacyCategoryChannels is idempotent`() { - createChannel("NodeEvent") - - systemNotificationManager.removeLegacyCategoryChannels() - systemNotificationManager.removeLegacyCategoryChannels() - - assertNull(systemNotificationManager.getNotificationChannel("NodeEvent")) - } - - @Test - fun `dispatch routes all categories to canonical channels`() = runTest { - val manager = AndroidNotificationManager(context) - - assertDispatchesToChannel(manager, Notification.Category.Message, NotificationChannels.MESSAGES) - assertDispatchesToChannel(manager, Notification.Category.NodeEvent, NotificationChannels.NEW_NODES) - assertDispatchesToChannel(manager, Notification.Category.Battery, NotificationChannels.LOW_BATTERY) - assertDispatchesToChannel(manager, Notification.Category.Alert, NotificationChannels.ALERTS) - assertDispatchesToChannel(manager, Notification.Category.Service, NotificationChannels.SERVICE) - } - - @Test - fun `dispatch attaches deep-link PendingIntent when deepLinkUri is set`() = runTest { - registerStubMainActivity() - val manager = AndroidNotificationManager(context) - val deepLink = "meshtastic://meshtastic/nodes/1234" - - manager.dispatch( - Notification( - title = "New node", - message = "Long Name", - category = Notification.Category.NodeEvent, - id = 1234, - deepLinkUri = deepLink, - ), - ) - - val posted = shadowOf(systemNotificationManager).allNotifications.last() - val pendingIntent = - requireNotNull(posted.contentIntent) { "Expected contentIntent to be set when deepLinkUri is provided" } - val shadowPendingIntent = shadowOf(pendingIntent) - val savedIntent = shadowPendingIntent.savedIntent - assertEquals(android.content.Intent.ACTION_VIEW, savedIntent.action) - assertEquals(deepLink, savedIntent.data?.toString()) - assertEquals("org.meshtastic.app.MainActivity", savedIntent.component?.className) - } - - @Test - fun `dispatch leaves contentIntent unset when deepLinkUri is null`() = runTest { - val manager = AndroidNotificationManager(context) - - manager.dispatch(Notification(title = "Plain", message = "No tap", category = Notification.Category.NodeEvent)) - - val posted = shadowOf(systemNotificationManager).allNotifications.last() - assertNull(posted.contentIntent) - } - - @Test - fun `dispatch uses provided notification id as system id`() = runTest { - val manager = AndroidNotificationManager(context) - val explicitId = 4242 - - manager.dispatch( - Notification( - title = "With id", - message = "explicit", - category = Notification.Category.NodeEvent, - id = explicitId, - ), - ) - - // Cancellation by the same id should remove the posted notification. - assertEquals(1, shadowOf(systemNotificationManager).allNotifications.size) - manager.cancel(explicitId) - assertEquals(0, shadowOf(systemNotificationManager).allNotifications.size) - } - - @Test - fun `client notification identity cancels the notification posted with that identity`() = runTest { - val manager = AndroidNotificationManager(context) - val clientNotification = - ClientNotification.Builder() - .also { wb -> - wb.message = "Protected position advisory" - wb.reply_id = 123 - } - .build() - val id = clientNotification.notificationId() - - manager.dispatch( - Notification( - title = "Client notification", - message = clientNotification.message, - category = Notification.Category.Alert, - id = id, - ), - ) - - assertEquals(1, shadowOf(systemNotificationManager).allNotifications.size) - manager.cancel(clientNotification.notificationId()) - assertEquals(0, shadowOf(systemNotificationManager).allNotifications.size) - } - - @Test - fun `exact protected position advisory is recognized`() { - val manager = AndroidNotificationManager(context) - - assertTrue(manager.suppressClientNotificationModal(protectedPositionAdvisory(replyId = 123, time = 1_000))) - } - - @Test - fun `protected position advisory predicate rejects every near miss`() { - val manager = AndroidNotificationManager(context) - val advisory = protectedPositionAdvisory(replyId = 123, time = 1_000) - val nearMisses = - listOf( - advisory.newBuilder().also { wb -> wb.message = "Location sharing is disabled" }.build(), - advisory.newBuilder().also { wb -> wb.level = LogRecord.Level.INFO }.build(), - advisory.newBuilder().also { wb -> wb.reply_id = 0 }.build(), - advisory.newBuilder().also { wb -> wb.reply_id = null }.build(), - advisory - .newBuilder() - .also { wb -> wb.key_verification_number_inform = KeyVerificationNumberInform.Builder().build() } - .build(), - advisory - .newBuilder() - .also { wb -> wb.key_verification_number_request = KeyVerificationNumberRequest.Builder().build() } - .build(), - advisory - .newBuilder() - .also { wb -> wb.key_verification_final = KeyVerificationFinal.Builder().build() } - .build(), - advisory - .newBuilder() - .also { wb -> wb.duplicated_public_key = DuplicatedPublicKey.Builder().build() } - .build(), - advisory.newBuilder().also { wb -> wb.low_entropy_key = LowEntropyKey.Builder().build() }.build(), - advisory.newBuilder().also { wb -> wb.message = "Rebooting to WiFi OTA" }.build(), - ) - - nearMisses.forEach { notification -> - assertFalse(manager.suppressClientNotificationModal(notification), notification.toString()) - } - } - - @Test - fun `protected position advisories replace one stable only-alert-once system notification`() = runTest { - val manager = AndroidNotificationManager(context) - val first = protectedPositionAdvisory(replyId = 123, time = 1_000) - val second = protectedPositionAdvisory(replyId = 456, time = 2_000) - - listOf(first, second).forEach { advisory -> - manager.dispatchClientNotification( - Notification( - title = "Client notification", - message = advisory.message, - category = Notification.Category.Alert, - id = advisory.notificationId(), - ), - advisory, - ) - } - - val posted = shadowOf(systemNotificationManager).allNotifications.single() - assertTrue(posted.flags and android.app.Notification.FLAG_ONLY_ALERT_ONCE != 0) - val active = systemNotificationManager.activeNotifications.single() - assertEquals(first.notificationId(), active.id) - } - - @Test - fun `repeated generic client notifications with the same message coalesce into one tray entry`() = runTest { - val manager = AndroidNotificationManager(context) - // A near-miss of the protected-position predicate (message differs slightly), so it takes the plain - // dispatch path — but reply_id/time still change on every firmware reply, exactly like the real advisory. - val first = - ClientNotification.Builder() - .also { wb -> - wb.message = "Location sharing is disabled" - wb.reply_id = 100 - wb.time = 1_000 - } - .build() - val second = - ClientNotification.Builder() - .also { wb -> - wb.message = "Location sharing is disabled" - wb.reply_id = 200 - wb.time = 2_000 - } - .build() - - listOf(first, second).forEach { cn -> - manager.dispatchClientNotification( - Notification( - title = "Client notification", - message = cn.message, - category = Notification.Category.Alert, - id = cn.notificationId(), - ), - cn, - ) - } - - assertEquals(1, shadowOf(systemNotificationManager).allNotifications.size) - } - - @Test - fun `generic client notification does not enable only-alert-once`() = runTest { - val manager = AndroidNotificationManager(context) - val clientNotification = - ClientNotification.Builder() - .also { wb -> - wb.message = "Generic warning" - wb.reply_id = 123 - } - .build() - - manager.dispatchClientNotification( - Notification( - title = "Client notification", - message = clientNotification.message, - category = Notification.Category.Alert, - id = clientNotification.notificationId(), - ), - clientNotification, - ) - - val posted = shadowOf(systemNotificationManager).allNotifications.single() - assertEquals(0, posted.flags and android.app.Notification.FLAG_ONLY_ALERT_ONCE) - } - - private suspend fun assertDispatchesToChannel( - manager: AndroidNotificationManager, - category: Notification.Category, - expectedChannelId: String, - ) { - systemNotificationManager.cancelAll() - manager.dispatch( - Notification(title = "Title-${category.name}", message = "Message-${category.name}", category = category), - ) - - val posted = shadowOf(systemNotificationManager).allNotifications.last() - assertEquals(expectedChannelId, posted.channelId) - } - - private fun createChannel(id: String, importance: Int = NotificationManager.IMPORTANCE_DEFAULT) { - systemNotificationManager.createNotificationChannel(NotificationChannel(id, id, importance)) - } - - /** - * Registers a stub `org.meshtastic.app.MainActivity` with the Robolectric `PackageManager` so that - * `TaskStackBuilder.addNextIntentWithParentStack` does not throw `NameNotFoundException` when resolving the - * activity that hosts deep-link intents. The real activity lives in `:androidApp`, which is intentionally not on - * `:core:service`'s test classpath. - */ - private fun registerStubMainActivity() { - val componentName = android.content.ComponentName(context, "org.meshtastic.app.MainActivity") - val activityInfo = - android.content.pm.ActivityInfo().apply { - name = componentName.className - packageName = componentName.packageName - exported = true - } - shadowOf(context.packageManager).addOrUpdateActivity(activityInfo) - } - - private fun clearManagedChannels() { - val channelIds = - NotificationChannels.LEGACY_CATEGORY_IDS + - listOf( - NotificationChannels.SERVICE, - NotificationChannels.MESSAGES, - NotificationChannels.BROADCASTS, - NotificationChannels.WAYPOINTS, - NotificationChannels.ALERTS, - NotificationChannels.NEW_NODES, - NotificationChannels.LOW_BATTERY, - NotificationChannels.LOW_BATTERY_REMOTE, - NotificationChannels.CLIENT, - ) - - channelIds.forEach { channelId -> systemNotificationManager.deleteNotificationChannel(channelId) } - } - - private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification.Builder() - .also { wb -> - wb.message = "Location sharing is disabled on this channel" - wb.reply_id = replyId - wb.time = time - wb.level = LogRecord.Level.WARNING - } - .build() -} diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/BootCompleteReceiverTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/BootCompleteReceiverTest.kt index 2490ef6ca7..a29fcbabdf 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/BootCompleteReceiverTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/BootCompleteReceiverTest.kt @@ -39,9 +39,9 @@ import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.dsl.module import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MeshPrefs -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.testing.FakeMeshNotificationManager import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -101,10 +101,8 @@ class BootCompleteReceiverTest { advanceUntilIdle() assertTrue(shadowOf(application).allStartedServices.isEmpty(), "must not start a service that cannot connect") - assertEquals(1, recordingNotifications.dispatched.size) - assertEquals(Notification.Type.Warning, recordingNotifications.dispatched.single().type) - assertTrue(recordingNotifications.dispatched.single().deepLinkUri?.contains("connections") == true) - assertTrue(recordingNotifications.dispatched.single().message.contains("Bluetooth")) + val (_, message) = recordingNotifications.reconnectBlocked.single() + assertTrue(message.contains("Bluetooth")) } @Test @@ -120,7 +118,7 @@ class BootCompleteReceiverTest { advanceUntilIdle() assertEquals(1, shadowOf(application).allStartedServices.size) - assertTrue(recordingNotifications.dispatched.isEmpty()) + assertTrue(recordingNotifications.reconnectBlocked.isEmpty()) } @Test @@ -205,7 +203,7 @@ class BootCompleteReceiverTest { modules( module { single { meshPrefs } - single { recordingNotifications } + single { recordingNotifications } single { CoroutineDispatchers(io = ioDispatcher, main = defaultDispatcher, default = defaultDispatcher) } @@ -215,20 +213,7 @@ class BootCompleteReceiverTest { } /** Captures what the receiver tried to tell the user when it declined to start the service. */ - private val recordingNotifications = RecordingNotificationManager() - - private class RecordingNotificationManager : NotificationManager { - val dispatched = mutableListOf() - - override suspend fun dispatch(notification: Notification): Boolean { - dispatched += notification - return true - } - - override fun cancel(id: Int) = Unit - - override fun cancelAll() = Unit - } + private val recordingNotifications = FakeMeshNotificationManager() /** * Grants BLUETOOTH_CONNECT unless told otherwise. Robolectric denies runtime permissions by default, and a boot diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationActionServiceTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationActionServiceTest.kt new file mode 100644 index 0000000000..aca06c3a01 --- /dev/null +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationActionServiceTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.content.Intent +import android.os.Bundle +import androidx.core.app.RemoteInput +import androidx.test.core.app.ApplicationProvider +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify.VerifyMode +import dev.mokkery.verifySuspend +import kotlinx.coroutines.Dispatchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioController +import org.meshtastic.core.repository.usecase.SendMessageOutcome +import org.meshtastic.core.repository.usecase.SendMessageUseCase +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class ConversationActionServiceTest { + + private val sendMessageUseCase: SendMessageUseCase = mock(MockMode.autofill) + private val packetRepository: PacketRepository = mock(MockMode.autofill) + private val radioController: RadioController = mock(MockMode.autofill) + private val notifications: MeshNotificationManager = mock(MockMode.autofill) + private val contactKey = "0!12345678" + + @Before + fun setUp() { + startKoin { + modules( + module { + single { sendMessageUseCase } + single { packetRepository } + single { radioController } + single { notifications } + // Unconfined so each action completes inside startCommand. + single { + CoroutineDispatchers( + io = Dispatchers.Unconfined, + main = Dispatchers.Unconfined, + default = Dispatchers.Unconfined, + ) + } + }, + ) + } + } + + @After + fun tearDown() { + stopKoin() + } + + private fun run(intent: Intent) { + Robolectric.buildService(ConversationActionService::class.java, intent).create().startCommand(0, 1) + } + + private fun intent(action: String) = + Intent(ApplicationProvider.getApplicationContext(), ConversationActionService::class.java) + .setAction(action) + .putExtra(ConversationActionService.EXTRA_CONTACT_KEY, contactKey) + + private fun replyIntent(text: String): Intent { + val intent = intent(ConversationActionService.ACTION_REPLY) + val results = Bundle().apply { putCharSequence(ConversationActionService.KEY_TEXT_REPLY, text) } + RemoteInput.addResultsToIntent( + arrayOf(RemoteInput.Builder(ConversationActionService.KEY_TEXT_REPLY).build()), + intent, + results, + ) + return intent + } + + @Test + fun `a reply is sent, marks the conversation read and refreshes it in place`() { + everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns SendMessageOutcome.Queued(1) + + run(replyIntent("hello back")) + + verifySuspend { sendMessageUseCase.invoke("hello back", contactKey, null) } + verifySuspend { packetRepository.clearUnreadCount(contactKey, any()) } + verifySuspend { notifications.refreshConversationAfterReply(contactKey) } + verifySuspend(VerifyMode.exactly(0)) { notifications.cancelMessageNotification(any()) } + } + + @Test + fun `a refused reply dismisses without marking read or showing it as sent`() { + everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns SendMessageOutcome.Refused + + run(replyIntent("hi")) + + verifySuspend(VerifyMode.exactly(0)) { packetRepository.clearUnreadCount(any(), any()) } + verifySuspend(VerifyMode.exactly(0)) { notifications.refreshConversationAfterReply(any()) } + verifySuspend { notifications.cancelMessageNotification(contactKey) } + } + + @Test + fun `a failed reply still dismisses so the reply spinner resolves`() { + everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } throws RuntimeException("radio down") + + run(replyIntent("hi")) + + verifySuspend(VerifyMode.exactly(0)) { packetRepository.clearUnreadCount(any(), any()) } + verifySuspend { notifications.cancelMessageNotification(contactKey) } + } + + @Test + fun `a reply without text sends nothing`() { + run(intent(ConversationActionService.ACTION_REPLY)) + + verifySuspend(VerifyMode.exactly(0)) { sendMessageUseCase.invoke(any(), any(), any()) } + } + + @Test + fun `mark as read clears the unread count and dismisses the conversation`() { + run(intent(ConversationActionService.ACTION_MARK_AS_READ)) + + verifySuspend { packetRepository.clearUnreadCount(contactKey, any()) } + verifySuspend { notifications.cancelMessageNotification(contactKey) } + } + + @Test + fun `a thumbs-up is sent and shown in the conversation`() { + run( + intent(ConversationActionService.ACTION_REACT) + .putExtra(ConversationActionService.EXTRA_REPLY_ID, 42) + .putExtra(ConversationActionService.EXTRA_EMOJI, "👍"), + ) + + verifySuspend { radioController.sendReaction("👍", 42, contactKey) } + verifySuspend { notifications.refreshConversationAfterReply(contactKey) } + } + + @Test + fun `a failed thumbs-up leaves the conversation in the tray`() { + everySuspend { radioController.sendReaction(any(), any(), any()) } throws RuntimeException("radio down") + + run( + intent(ConversationActionService.ACTION_REACT) + .putExtra(ConversationActionService.EXTRA_REPLY_ID, 42) + .putExtra(ConversationActionService.EXTRA_EMOJI, "👍"), + ) + + verifySuspend(VerifyMode.exactly(0)) { notifications.cancelMessageNotification(any()) } + verifySuspend(VerifyMode.exactly(0)) { notifications.refreshConversationAfterReply(any()) } + } +} diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt index c29a1b9dcc..a02267aa5d 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt @@ -38,6 +38,9 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getStringSuspend +import org.meshtastic.core.resources.unknown_username import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.User @@ -127,7 +130,7 @@ class ConversationShortcutPublisherTest { } @Test - fun `dm shortcuts carry node labels and the empty primary resolves to its preset name`() = runTest { + fun `dm shortcuts are titled by the node long name and the empty primary by its preset name`() = runTest { every { packetRepository.getContacts() } returns flowOf(mapOf("0!00000007" to contact(from = "!00000007", time = 1_000))) @@ -135,12 +138,27 @@ class ConversationShortcutPublisherTest { advanceUntilIdle() val byId = shortcutManager.dynamicShortcuts.associateBy { it.id } - assertEquals("HAWK", byId.getValue("0!00000007").shortLabel) + assertEquals("Hawk Ridge", byId.getValue("0!00000007").shortLabel) assertEquals("Hawk Ridge", byId.getValue("0!00000007").longLabel) assertEquals("LongFast", byId.getValue("0^all").shortLabel) assertEquals("Beta", byId.getValue("1^all").shortLabel) } + @Test + fun `dm shortcut for a node missing from the node db is titled with the unknown-user name`() = runTest { + every { packetRepository.getContacts() } returns + flowOf(mapOf("0!000000fe" to contact(from = "!000000fe", time = 1_000))) + // Resolved first so the resources cache is loaded; a cold load runs on Dispatchers.Default, which + // advanceUntilIdle does not wait for. + val expected = getStringSuspend(Res.string.unknown_username) + + publisher.startObserving(this) + advanceUntilIdle() + + val published = shortcutManager.dynamicShortcuts.first { it.id == "0!000000fe" } + assertEquals(expected, published.shortLabel) + } + @Test fun `stale shortcuts are removed when no longer part of the conversation set`() = runTest { // Simulate a leftover shortcut from a previous session (raw push: the in-memory on-demand protection does not diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt index d21d2c1f97..e880776b05 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt @@ -152,7 +152,7 @@ class MeshLogCleanupWorkerTest { workerClassName: String, workerParameters: WorkerParameters, ): ListenableWorker = - MeshLogCleanupWorker(appContext, workerParameters, repository, meshLogPrefs) + MeshLogCleanupWorker(appContext, workerParameters, MeshLogCleanup(repository, meshLogPrefs)) }, ) .build() diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt index 3c388d43a9..1520f0e9f0 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt @@ -18,7 +18,12 @@ package org.meshtastic.core.service import android.app.Notification import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.os.Parcelable +import androidx.core.app.NotificationCompat +import androidx.core.app.RemoteInput +import androidx.core.os.BundleCompat import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.mokkery.MockMode @@ -42,6 +47,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Message import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository @@ -49,6 +55,7 @@ import org.meshtastic.core.testing.runUntilSettled import org.meshtastic.core.testing.runWithRenderScope import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.User +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -116,6 +123,7 @@ class MeshNotificationManagerImplConversationTest { }, radioConfigRepository = lazy { radioConfigRepository }, radioOperationLock = RadioOperationLock(), + firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository(), scope = scope.asServiceScope(), ) @@ -141,7 +149,7 @@ class MeshNotificationManagerImplConversationTest { /** Newest-first message history, mirroring the repository's ordering. */ private fun mockHistory(vararg messages: Message) { - everySuspend { packetRepository.getMessagesFrom(any(), any(), any(), any()) } returns flowOf(messages.toList()) + every { packetRepository.getMessagesFrom(any(), any(), any(), any()) } returns flowOf(messages.toList()) } private fun message(text: String, read: Boolean, receivedTime: Long): Message = Message( @@ -203,6 +211,11 @@ class MeshNotificationManagerImplConversationTest { val bubble = posted.bubbleMetadata assertNotNull(bubble, "conversation notifications must offer a bubble") assertNotNull(bubble.icon, "a bubble without an icon is rejected") + assertEquals( + android.graphics.drawable.Icon.TYPE_ADAPTIVE_BITMAP, + bubble.icon?.type, + "Android 10 rejects a plain bitmap bubble icon", + ) assertEquals("0^all", posted.shortcutId, "the bubble needs its long-lived conversation shortcut") assertTrue( posted.extras.containsKey(Notification.EXTRA_PEOPLE_LIST), @@ -233,6 +246,104 @@ class MeshNotificationManagerImplConversationTest { ) } + @Test + fun `conversation actions work from a watch without opening the phone`() = runWithRenderScope { scope -> + val manager = createManager(scope).also { it.initChannels() } + mockHistory(message("hello", read = false, receivedTime = 1_000)) + + manager.updateMessageNotification("0^all", "Hawk Ridge", "hello", isBroadcast = true, channelName = "LongFast") + advanceUntilIdle() + + val actions = activeByTag("message").single().notification.actions.orEmpty() + val reply = actions.single { it.semanticAction == Notification.Action.SEMANTIC_ACTION_REPLY } + assertTrue(reply.allowGeneratedReplies, "Smart Reply suggestions on a watch need generated replies allowed") + val thumbsUp = actions.single { it.semanticAction == Notification.Action.SEMANTIC_ACTION_THUMBS_UP } + assertEquals(false, thumbsUp.extras.getBoolean(SHOWS_USER_INTERFACE, true)) + assertTrue(actions.any { it.semanticAction == Notification.Action.SEMANTIC_ACTION_MARK_AS_READ }) + } + + /** + * The contract Android Auto checks before it shows a conversation in the car: a MessagingStyle naming the device + * user, a reply action with exactly one RemoteInput whose mutable PendingIntent reaches a Service without opening + * UI, and a mark-as-read action that opens no UI either. + */ + @Test + fun `a direct message meets Android Auto's messaging contract`() = runWithRenderScope { scope -> + val manager = createManager(scope).also { it.initChannels() } + mockHistory(message("hello", read = false, receivedTime = 1_000)) + + manager.updateMessageNotification("0!abcd1234", "Hawk Ridge", "hello", isBroadcast = false, channelName = null) + advanceUntilIdle() + + val posted = activeByTag("message").single().notification + assertEquals(Notification.CATEGORY_MESSAGE, posted.category) + val style = assertNotNull(NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(posted)) + assertNotNull(style.user.name, "the device user is named, and read aloud in the car") + assertNull(style.conversationTitle, "a one-to-one chat has no title; a title marks a group") + assertEquals(false, style.isGroupConversation) + assertEquals("Hawk Ridge", style.messages.single().person?.name?.toString()) + + val actions = posted.actions.orEmpty() + val reply = actions.single { it.semanticAction == Notification.Action.SEMANTIC_ACTION_REPLY } + val markAsRead = actions.single { it.semanticAction == Notification.Action.SEMANTIC_ACTION_MARK_AS_READ } + listOf(reply, markAsRead).forEach { action -> + assertEquals(false, action.extras.getBoolean(SHOWS_USER_INTERFACE, true)) + val pendingIntent = shadowOf(action.actionIntent) + assertTrue(pendingIntent.isService, "Android Auto has these actions handled by a Service") + assertEquals(ConversationActionService::class.java.name, pendingIntent.savedIntent.component?.className) + } + assertEquals(1, reply.remoteInputs?.size) + assertTrue(shadowOf(reply.actionIntent).flags and PendingIntent.FLAG_MUTABLE != 0, "Auto fills the reply in") + assertTrue(shadowOf(markAsRead.actionIntent).flags and PendingIntent.FLAG_IMMUTABLE != 0) + assertTrue(reply.actionIntent != markAsRead.actionIntent) + + // Fire the reply the way the car does: the RemoteInput text filled into the mutable PendingIntent. + val fillIn = android.content.Intent() + val results = + android.os.Bundle().apply { putCharSequence(reply.remoteInputs!!.single().resultKey, "on my way") } + android.app.RemoteInput.addResultsToIntent(reply.remoteInputs, fillIn, results) + reply.actionIntent.send(context, 0, fillIn) + val started = assertNotNull(shadowOf(context as android.app.Application).nextStartedService) + assertEquals(ConversationActionService.ACTION_REPLY, started.action) + assertEquals("0!abcd1234", started.getStringExtra(ConversationActionService.EXTRA_CONTACT_KEY)) + assertEquals( + "on my way", + RemoteInput.getResultsFromIntent(started) + ?.getCharSequence(ConversationActionService.KEY_TEXT_REPLY) + ?.toString(), + ) + } + + @Test + fun `a channel message is a titled group conversation`() = runWithRenderScope { scope -> + val manager = createManager(scope).also { it.initChannels() } + mockHistory(message("hello", read = false, receivedTime = 1_000)) + + manager.updateMessageNotification("0^all", "Hawk Ridge", "hello", isBroadcast = true, channelName = "LongFast") + advanceUntilIdle() + + val style = + assertNotNull( + NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification( + activeByTag("message").single().notification, + ), + ) + assertEquals("LongFast", style.conversationTitle?.toString()) + assertEquals(true, style.isGroupConversation) + } + + @Test + @Config(sdk = [29]) + fun `conversation notifications post on Android 10`() = runWithRenderScope { scope -> + val manager = createManager(scope).also { it.initChannels() } + mockHistory(message("hello", read = false, receivedTime = 1_000)) + + manager.updateMessageNotification("0^all", "Hawk Ridge", "hello", isBroadcast = true, channelName = "LongFast") + advanceUntilIdle() + + assertNotNull(activeByTag("message").single().notification.bubbleMetadata) + } + @Test fun `read messages become historic context and unread messages stay alerting`() = runWithRenderScope { scope -> val manager = createManager(scope).also { it.initChannels() } @@ -252,8 +363,10 @@ class MeshNotificationManagerImplConversationTest { advanceUntilIdle() val posted = activeByTag("message").single().notification - val alerting = posted.extras.getParcelableArray(Notification.EXTRA_MESSAGES) - val historic = posted.extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + val alerting = + BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_MESSAGES, Parcelable::class.java) + val historic = + BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_HISTORIC_MESSAGES, Parcelable::class.java) assertEquals(1, alerting?.size, "only the unread message should be presented as new content") assertEquals(2, historic?.size, "read context should be carried as historic messages") assertEquals("new unread", posted.extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()) @@ -276,13 +389,12 @@ class MeshNotificationManagerImplConversationTest { val summary = activeByTag("message_summary").single().notification assertEquals(Notification.GROUP_ALERT_CHILDREN, summary.groupAlertBehavior) - // The summary line is rebuilt from the child's real MessagingStyle, so it carries the actual sender. - val summaryLatest = - androidx.core.app.NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(summary) - ?.messages - ?.lastOrNull() - assertEquals("hello", summaryLatest?.text?.toString()) - assertEquals("Hawk Ridge", summaryLatest?.person?.name?.toString()) + // The summary line is rebuilt from the child's real MessagingStyle, so it carries the actual sender, but + // the + // summary itself is not a conversation Android Auto could try to answer. + assertNull(NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(summary)) + val lines = summary.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES)?.map { it.toString() } + assertEquals(listOf("Hawk Ridge: hello"), lines) manager.cancelMessageNotification("0^all") @@ -296,7 +408,7 @@ class MeshNotificationManagerImplConversationTest { val manager = createManager(scope).also { it.initChannels() } // SERVICE_NOTIFY_ID is 101; a node whose num is also 101 used to overwrite the foreground notification. manager.updateServiceStateNotification(ConnectionState.Connected, telemetry = null) - manager.showOrUpdateLowBatteryNotification(Node(num = 101), isRemote = false) + manager.showLowBatteryNotification(Node(num = 101), isRemote = false) runUntilSettled { systemNotificationManager.activeNotifications.any { it.id == 101 && it.tag == null } && activeByTag("low_battery").any { it.id == 101 } @@ -321,7 +433,9 @@ class MeshNotificationManagerImplConversationTest { val posted = activeByTag("message").single().notification // Empty primary channel resolves to its modem-preset display name, matching the in-app conversation list. assertEquals("LongFast", posted.extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)?.toString()) - assertNotNull(posted.extras.getParcelableArray(Notification.EXTRA_MESSAGES)) + assertNotNull( + BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_MESSAGES, Parcelable::class.java), + ) } @Test @@ -343,3 +457,6 @@ class MeshNotificationManagerImplConversationTest { assertEquals("Hawk Ridge", shortcut.shortLabel) } } + +/** NotificationCompat stores an action's showsUserInterface flag in its extras under this key. */ +private const val SHOWS_USER_INTERFACE = "android.support.action.showsUserInterface" diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplRoutingTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplRoutingTest.kt new file mode 100644 index 0000000000..c2fa9621d5 --- /dev/null +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplRoutingTest.kt @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.service.notification.StatusBarNotification +import androidx.core.app.NotificationCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.meshtastic.core.common.di.asServiceScope +import org.meshtastic.core.common.state.RadioOperationLock +import org.meshtastic.core.model.FirmwareUpdateDestination +import org.meshtastic.core.model.FirmwareUpdateNotice +import org.meshtastic.core.model.MeshBeaconOffer +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository +import org.meshtastic.core.repository.notificationId +import org.meshtastic.core.testing.runWithRenderScope +import org.meshtastic.proto.ClientNotification +import org.meshtastic.proto.DuplicatedPublicKey +import org.meshtastic.proto.KeyVerificationFinal +import org.meshtastic.proto.KeyVerificationNumberInform +import org.meshtastic.proto.KeyVerificationNumberRequest +import org.meshtastic.proto.LogRecord +import org.meshtastic.proto.LowEntropyKey +import org.meshtastic.proto.MeshBeacon +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.meshtastic.core.repository.Notification as MeshNotification + +/** Every notification that is not a conversation: its channel, tray slot, tap target and alerting flags. */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [34]) +class MeshNotificationManagerImplRoutingTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val systemNotificationManager = context.getSystemService(NotificationManager::class.java)!! + + @Before + fun setUp() { + val main = ComponentName(context, "org.meshtastic.app.MainActivity") + val activityInfo = + ActivityInfo().apply { + name = main.className + packageName = main.packageName + exported = true + } + shadowOf(context.packageManager).addOrUpdateActivity(activityInfo) + systemNotificationManager.cancelAll() + NotificationChannelSpec.entries.forEach { systemNotificationManager.deleteNotificationChannel(it.id) } + } + + @After + fun tearDown() { + systemNotificationManager.cancelAll() + } + + /** + * Android Auto treats a MessagingStyle notification as a conversation it can read aloud and answer, and requires + * reply and mark-as-read on it. Only conversations carry those, so nothing else may be MessagingStyle. + */ + @Test + fun `nothing but a conversation looks like one to Android Auto`() = runWithRenderScope { scope -> + val manager = createManager(scope) + manager.updateWaypointNotification("0^all", "Hawk Ridge", "Camp", waypointId = 42) + manager.showAlertNotification("0!abcd1234", "Hawk Ridge", "Fire at camp") + manager.showMeshBeaconNotification( + MeshBeaconOffer(fromNodeNum = 7, beacon = MeshBeacon.Builder().also { wb -> wb.message = "Join" }.build()), + ) + manager.showNewNodeSeenNotification(Node(num = 101), "New node seen: N101") + manager.showLowBatteryNotification(Node(num = 2), isRemote = true) + manager.showClientNotification( + ClientNotification.Builder().also { wb -> wb.message = "Duplicate key" }.build(), + "Radio notice", + MeshNotification.Type.Warning, + ) + manager.showFirmwareUpdateNotice(FirmwareUpdateDestination.AndroidUpdate) + manager.showReconnectBlockedNotification("Meshtastic can't reconnect", "Bluetooth is off") + + val posted = systemNotificationManager.activeNotifications + assertEquals(8, posted.size) + posted.forEach { sbn -> + assertNull( + NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(sbn.notification), + "${sbn.tag} must not be MessagingStyle", + ) + } + } + + @Test + fun `a received waypoint posts on the waypoint channel and opens the map at it`() = runWithRenderScope { scope -> + createManager(scope).updateWaypointNotification("0^all", "Hawk Ridge", "Camp", waypointId = 42) + + val posted = activeByTag("waypoint").single().notification + assertEquals(NotificationChannels.WAYPOINTS, posted.channelId) + assertNull(posted.group, "a waypoint is not a conversation and must stay out of the messages summary") + assertEquals("meshtastic://meshtastic/map?waypointId=42", tapTarget(posted)) + } + + @Test + fun `a reaction posts on the reaction channel and is cleared with its conversation`() = + runWithRenderScope { scope -> + val manager = createManager(scope) + + manager.updateReactionNotification("0!abcd1234", "Hawk Ridge", "👍", false, null, false) + + val posted = activeByTag("reaction").single().notification + assertEquals(NotificationChannels.REACTIONS, posted.channelId) + assertEquals("👍", posted.extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()) + assertNull(posted.group, "a reaction is not a conversation and must stay out of the messages summary") + assertEquals("meshtastic://meshtastic/messages/0!abcd1234", tapTarget(posted)) + + manager.cancelMessageNotification("0!abcd1234") + assertTrue(activeByTag("reaction").isEmpty()) + } + + @Test + fun `a critical alert posts as an alarm that opens its conversation`() = runWithRenderScope { scope -> + createManager(scope).showAlertNotification("0!abcd1234", "Hawk Ridge", "Fire at camp") + + val posted = activeByTag("alert").single().notification + assertEquals(NotificationChannels.ALERTS, posted.channelId) + assertEquals(Notification.CATEGORY_ALARM, posted.category) + assertEquals("meshtastic://meshtastic/messages/0!abcd1234", tapTarget(posted)) + } + + @Test + fun `a mesh invitation posts on its own channel and opens discovery`() = runWithRenderScope { scope -> + val offer = + MeshBeaconOffer(fromNodeNum = 7, beacon = MeshBeacon.Builder().also { wb -> wb.message = "Join" }.build()) + + createManager(scope).showMeshBeaconNotification(offer) + + val posted = activeByTag("mesh_beacon").single() + assertEquals(7, posted.id) + assertEquals(NotificationChannels.MESH_BEACON, posted.notification.channelId) + assertEquals("meshtastic://meshtastic/discovery", tapTarget(posted.notification)) + } + + @Test + fun `a new node posts under its own tag and is cancelled by node number`() = runWithRenderScope { scope -> + val manager = createManager(scope) + + manager.showNewNodeSeenNotification(Node(num = 101), "New node seen: N101") + + val posted = activeByTag("new_node").single() + assertEquals(101, posted.id) + assertEquals(NotificationChannels.NEW_NODES, posted.notification.channelId) + assertEquals("New node seen: N101", posted.notification.extras.getString(Notification.EXTRA_TITLE)) + assertEquals("meshtastic://meshtastic/nodes/101", tapTarget(posted.notification)) + + manager.cancelNewNodeNotification(101) + assertTrue(activeByTag("new_node").isEmpty()) + } + + @Test + fun `low battery posts on the channel for whose battery it is`() = runWithRenderScope { scope -> + val manager = createManager(scope) + + manager.showLowBatteryNotification(Node(num = 1), isRemote = false) + manager.showLowBatteryNotification(Node(num = 2), isRemote = true) + + val byNode = activeByTag("low_battery").associateBy { it.id } + assertEquals(NotificationChannels.LOW_BATTERY, byNode.getValue(1).notification.channelId) + assertEquals(NotificationChannels.LOW_BATTERY_REMOTE, byNode.getValue(2).notification.channelId) + assertEquals("meshtastic://meshtastic/nodes/2", tapTarget(byNode.getValue(2).notification)) + // Ongoing notifications never bridge to a watch. + assertEquals(0, byNode.getValue(1).notification.flags and Notification.FLAG_ONGOING_EVENT) + } + + @Test + fun `a low-battery refresh never brings back a dismissed warning`() = runWithRenderScope { scope -> + val manager = createManager(scope) + val node = Node(num = 3) + + manager.showLowBatteryNotification(node, isRemote = false) + manager.cancelLowBatteryNotification(node) + manager.updateLowBatteryNotification(node, isRemote = false) + + assertTrue(activeByTag("low_battery").isEmpty()) + } + + @Test + fun `a warning still building when the battery recovers is never posted`() = runWithRenderScope { scope -> + val manager = createManager(scope) + val node = Node(num = 4) + manager.beforeLowBatteryPost = { manager.cancelLowBatteryNotification(node) } + + manager.showLowBatteryNotification(node, isRemote = false) + + assertTrue(activeByTag("low_battery").isEmpty()) + } + + @Test + fun `radio notices post on their own channel and clear by identity`() = runWithRenderScope { scope -> + val manager = createManager(scope) + val notice = ClientNotification.Builder().also { wb -> wb.message = "Generic warning" }.build() + + manager.showClientNotification(notice, "Radio notice", MeshNotification.Type.Warning) + + val posted = activeByTag("client").single() + assertEquals(notice.notificationId(), posted.id) + assertEquals(NotificationChannels.CLIENT, posted.notification.channelId) + assertEquals(0, posted.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) + + manager.clearClientNotification(notice) + assertTrue(activeByTag("client").isEmpty()) + } + + @Test + fun `repeated position advisories share one tray slot and alert once`() = runWithRenderScope { scope -> + val manager = createManager(scope) + val first = protectedPositionAdvisory(replyId = 123, time = 1_000) + val second = protectedPositionAdvisory(replyId = 456, time = 2_000) + + listOf(first, second).forEach { manager.showClientNotification(it, "Radio notice", MeshNotification.Type.Info) } + + val posted = activeByTag("client").single() + assertTrue(posted.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) + assertTrue(manager.suppressClientNotificationModal(first)) + } + + @Test + fun `only the exact position advisory skips the in-app modal`() = runWithRenderScope { scope -> + val manager = createManager(scope) + val advisory = protectedPositionAdvisory(replyId = 123, time = 1_000) + val nearMisses = + listOf( + advisory.newBuilder().also { wb -> wb.message = "Location sharing is disabled" }.build(), + advisory.newBuilder().also { wb -> wb.level = LogRecord.Level.INFO }.build(), + advisory.newBuilder().also { wb -> wb.reply_id = 0 }.build(), + advisory.newBuilder().also { wb -> wb.reply_id = null }.build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_number_inform = KeyVerificationNumberInform.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_number_request = KeyVerificationNumberRequest.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_final = KeyVerificationFinal.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.duplicated_public_key = DuplicatedPublicKey.Builder().build() } + .build(), + advisory.newBuilder().also { wb -> wb.low_entropy_key = LowEntropyKey.Builder().build() }.build(), + advisory.newBuilder().also { wb -> wb.message = "Rebooting to WiFi OTA" }.build(), + ) + + assertTrue(manager.suppressClientNotificationModal(advisory)) + nearMisses.forEach { assertFalse(manager.suppressClientNotificationModal(it), it.toString()) } + } + + @Test + fun `a firmware update opens in-app firmware updates whatever its destination`() = runWithRenderScope { scope -> + val accepted = createManager(scope).showFirmwareUpdateNotice(FirmwareUpdateDestination.MeshtasticFlasher) + + assertTrue(accepted) + val posted = activeByTag("firmware_update").single().notification + assertEquals(NotificationChannels.DEVICE_STATUS, posted.channelId) + assertEquals("meshtastic://meshtastic/firmware/update", tapTarget(posted)) + } + + @Test + fun `a firmware update on a blocked channel reports that it was not shown`() = runWithRenderScope { scope -> + val manager = createManager(scope) + manager.ensureChannels() + systemNotificationManager.createNotificationChannel( + NotificationChannel(NotificationChannels.DEVICE_STATUS, "blocked", NotificationManager.IMPORTANCE_NONE), + ) + + assertFalse(manager.showFirmwareUpdateNotice(FirmwareUpdateDestination.AndroidUpdate)) + assertTrue(activeByTag("firmware_update").isEmpty()) + } + + @Test + fun `the reconnect-blocked warning replaces itself and opens connections`() = runWithRenderScope { scope -> + val manager = createManager(scope) + + repeat(2) { manager.showReconnectBlockedNotification("Meshtastic can't reconnect", "Bluetooth is off") } + + val posted = activeByTag("reconnect_blocked").single().notification + assertEquals(NotificationChannels.DEVICE_STATUS, posted.channelId) + assertEquals("meshtastic://meshtastic/connections", tapTarget(posted)) + } + + private suspend fun MeshNotificationManagerImpl.showFirmwareUpdateNotice(destination: FirmwareUpdateDestination) = + showFirmwareUpdateNotification( + FirmwareUpdateNotice( + notificationKey = "node|2.8.0", + currentVersion = "2.7.0", + stableVersion = "2.8.0", + destination = destination, + ), + ) + + private fun createManager(scope: CoroutineScope) = MeshNotificationManagerImpl( + context = context, + packetRepository = lazy { error("Not used in this test") }, + nodeRepository = lazy { error("Not used in this test") }, + conversationShortcutPublisher = lazy { error("Not used in this test") }, + radioConfigRepository = lazy { error("Not used in this test") }, + radioOperationLock = RadioOperationLock(), + firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository(), + scope = scope.asServiceScope(), + ) + + private fun activeByTag(tag: String): List = + systemNotificationManager.activeNotifications.filter { it.tag == tag } + + private fun tapTarget(notification: Notification): String? = + shadowOf(notification.contentIntent).savedIntent.data?.toString() + + private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification.Builder() + .also { wb -> + wb.message = PROTECTED_POSITION_ADVISORY_MESSAGE + wb.reply_id = replyId + wb.time = time + wb.level = LogRecord.Level.WARNING + } + .build() +} diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt index 102147b91f..f749950133 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import androidx.core.app.NotificationCompat import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.mokkery.MockMode @@ -27,8 +28,10 @@ import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.mock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.withContext import org.junit.After import org.junit.Before import org.junit.Test @@ -37,11 +40,15 @@ import org.meshtastic.core.common.di.asServiceScope import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.repository.FirmwareUpdateProgress +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.disconnected +import org.meshtastic.core.resources.firmware_update_in_progress import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.local_stats_nodes import org.meshtastic.core.testing.runUntilSettled @@ -61,6 +68,7 @@ class MeshNotificationManagerImplTest { private lateinit var context: Context private lateinit var systemNotificationManager: NotificationManager private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository() @Before fun setUp() { @@ -78,30 +86,79 @@ class MeshNotificationManagerImplTest { } @Test - fun `initChannels removes legacy categories and creates canonical channels`() = runWithRenderScope { renderScope -> - NotificationChannels.LEGACY_CATEGORY_IDS.forEach(::createChannel) - val notifications = createManager(renderScope) - notifications.initChannels() + fun `initChannels removes legacy categories and has the service channel ready at once`() = + runWithRenderScope { renderScope -> + NotificationChannels.LEGACY_CATEGORY_IDS.forEach(::createChannel) + val notifications = createManager(renderScope) + notifications.initChannels() - NotificationChannels.LEGACY_CATEGORY_IDS.forEach { legacyId -> - assertNull(systemNotificationManager.getNotificationChannel(legacyId)) + NotificationChannels.LEGACY_CATEGORY_IDS.forEach { legacyId -> + assertNull(systemNotificationManager.getNotificationChannel(legacyId)) + } + // The foreground-service notification is posted right after initChannels returns, and the platform (not + // Robolectric) rejects a channel whose group does not exist yet. + val service = assertNotNull(systemNotificationManager.getNotificationChannel(NotificationChannels.SERVICE)) + assertEquals(NotificationChannelGroupSpec.Device.id, service.group) + assertNotNull(systemNotificationManager.getNotificationChannelGroup(NotificationChannelGroupSpec.Device.id)) } - val canonicalChannelIds = - listOf( - NotificationChannels.SERVICE, - NotificationChannels.MESSAGES, - NotificationChannels.BROADCASTS, - NotificationChannels.WAYPOINTS, - NotificationChannels.ALERTS, - NotificationChannels.NEW_NODES, - NotificationChannels.LOW_BATTERY, - NotificationChannels.LOW_BATTERY_REMOTE, - NotificationChannels.CLIENT, - ) + /** + * Pins every channel's importance and group. Importance is fixed when a channel is first created, so changing one + * here only reaches new installs, and lowering one below what shipped also lowers it on unmodified existing + * installs: an edit to this table is a product decision, not a refactor. + */ + @Test + fun `every channel is created with its importance, group and description`() = runWithRenderScope { renderScope -> + // Labels load on real threads; under virtual time the label timeout would fire at once and fall back. + withContext(Dispatchers.Default) { createManager(renderScope).ensureChannels() } - canonicalChannelIds.forEach { channelId -> - assertNotNull(systemNotificationManager.getNotificationChannel(channelId)) + NotificationChannelSpec.entries.forEach { spec -> + val (importance, group) = + when (spec) { + NotificationChannelSpec.Service -> + NotificationManager.IMPORTANCE_LOW to NotificationChannelGroupSpec.Device + + NotificationChannelSpec.DirectMessages -> + NotificationManager.IMPORTANCE_HIGH to NotificationChannelGroupSpec.Messages + + NotificationChannelSpec.Broadcasts -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Messages + + NotificationChannelSpec.Waypoints -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Messages + + NotificationChannelSpec.Reactions -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Messages + + NotificationChannelSpec.Alerts -> + NotificationManager.IMPORTANCE_HIGH to NotificationChannelGroupSpec.Messages + + NotificationChannelSpec.NewNodes -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Mesh + + NotificationChannelSpec.MeshBeacon -> + NotificationManager.IMPORTANCE_LOW to NotificationChannelGroupSpec.Mesh + + NotificationChannelSpec.LowBatteryRemote -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Mesh + + NotificationChannelSpec.LowBattery -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Device + + NotificationChannelSpec.Client -> + NotificationManager.IMPORTANCE_HIGH to NotificationChannelGroupSpec.Device + + NotificationChannelSpec.DeviceStatus -> + NotificationManager.IMPORTANCE_DEFAULT to NotificationChannelGroupSpec.Device + } + val channel = assertNotNull(systemNotificationManager.getNotificationChannel(spec.id), spec.name) + assertEquals(importance, channel.importance, spec.name) + assertEquals(group.id, channel.group, spec.name) + assertEquals(getString(spec.nameRes), channel.name.toString(), spec.name) + assertEquals(getString(spec.descriptionRes), channel.description, spec.name) + } + NotificationChannelGroupSpec.entries.forEach { group -> + assertNotNull(systemNotificationManager.getNotificationChannelGroup(group.id), group.name) } } @@ -161,6 +218,33 @@ class MeshNotificationManagerImplTest { assertNotNull(activeServiceNotification()) } + @Test + fun `a running flash makes the service notification a promotable progress notification`() = + runWithRenderScope { renderScope -> + val notifications = createManager(renderScope) + notifications.initChannels() + notifications.updateServiceStateNotification(ConnectionState.Disconnected, populatedTelemetry()) + runUntilSettled { activeServiceNotification() != null } + + firmwareUpdateStatusRepository.publishProgress( + FirmwareUpdateProgress(UiText.DynamicString("Writing firmware"), percent = 42), + ) + runUntilSettled { serviceExtras()?.getInt(Notification.EXTRA_PROGRESS) == 42 } + + val posted = assertNotNull(activeServiceNotification()).notification + assertEquals(getString(Res.string.firmware_update_in_progress), serviceTitle()) + assertEquals("Writing firmware", serviceExtras()?.getCharSequence(Notification.EXTRA_TEXT)?.toString()) + assertTrue(NotificationCompat.isRequestPromotedOngoing(posted)) + assertEquals("42%", posted.extras.getString(NotificationCompat.EXTRA_SHORT_CRITICAL_TEXT)) + + firmwareUpdateStatusRepository.publishProgress(null) + runUntilSettled { + activeServiceNotification()?.notification?.let { !NotificationCompat.isRequestPromotedOngoing(it) } == + true + } + assertEquals(getString(Res.string.disconnected), serviceTitle()) + } + @Test fun `service state seeds local stats before the local node row is available`() = runWithRenderScope { renderScope -> val stats = @@ -190,6 +274,7 @@ class MeshNotificationManagerImplTest { conversationShortcutPublisher = lazy { error("Not used in this test") }, radioConfigRepository = lazy { error("Not used in this test") }, radioOperationLock = RadioOperationLock(), + firmwareUpdateStatusRepository = firmwareUpdateStatusRepository, scope = scope.asServiceScope(), ) @@ -209,6 +294,10 @@ class MeshNotificationManagerImplTest { private fun activeServiceNotification() = systemNotificationManager.activeNotifications.singleOrNull { it.id == SERVICE_NOTIFY_ID } + private fun serviceExtras() = activeServiceNotification()?.notification?.extras + + private fun serviceTitle() = serviceExtras()?.getCharSequence(Notification.EXTRA_TITLE)?.toString() + private fun createChannel(id: String) { systemNotificationManager.createNotificationChannel( NotificationChannel(id, id, NotificationManager.IMPORTANCE_DEFAULT), @@ -216,20 +305,7 @@ class MeshNotificationManagerImplTest { } private fun clearManagedChannels() { - val channelIds = - NotificationChannels.LEGACY_CATEGORY_IDS + - listOf( - NotificationChannels.SERVICE, - NotificationChannels.MESSAGES, - NotificationChannels.BROADCASTS, - NotificationChannels.WAYPOINTS, - NotificationChannels.ALERTS, - NotificationChannels.NEW_NODES, - NotificationChannels.LOW_BATTERY, - NotificationChannels.LOW_BATTERY_REMOTE, - NotificationChannels.CLIENT, - ) - + val channelIds = NotificationChannels.LEGACY_CATEGORY_IDS + NotificationChannelSpec.entries.map { it.id } channelIds.forEach { channelId -> systemNotificationManager.deleteNotificationChannel(channelId) } } } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ReplyReceiverTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ReplyReceiverTest.kt deleted file mode 100644 index 9d5efa5d58..0000000000 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ReplyReceiverTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.content.Intent -import android.os.Bundle -import androidx.core.app.RemoteInput -import androidx.test.core.app.ApplicationProvider -import dev.mokkery.MockMode -import dev.mokkery.answering.throws -import dev.mokkery.everySuspend -import dev.mokkery.matcher.any -import dev.mokkery.mock -import dev.mokkery.verify.VerifyMode -import dev.mokkery.verifySuspend -import kotlinx.coroutines.Dispatchers -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.repository.MeshNotificationManager -import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.usecase.SendMessageUseCase -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34]) -class ReplyReceiverTest { - - private val sendMessageUseCase: SendMessageUseCase = mock(MockMode.autofill) - private val notificationManager: MeshNotificationManager = mock(MockMode.autofill) - private val packetRepository: PacketRepository = mock(MockMode.autofill) - - @Before - fun setUp() { - startKoin { - modules( - module { - single { sendMessageUseCase } - single { notificationManager } - single { packetRepository } - // Unconfined so the receiver's launched coroutine completes before onReceive returns - single { - CoroutineDispatchers( - io = Dispatchers.Unconfined, - main = Dispatchers.Unconfined, - default = Dispatchers.Unconfined, - ) - } - }, - ) - } - } - - @After - fun tearDown() { - stopKoin() - } - - private fun replyIntent(contactKey: String, text: String): Intent { - val intent = Intent(ReplyReceiver.REPLY_ACTION).putExtra(ReplyReceiver.CONTACT_KEY, contactKey) - val results = Bundle().apply { putCharSequence(ReplyReceiver.KEY_TEXT_REPLY, text) } - RemoteInput.addResultsToIntent( - arrayOf(RemoteInput.Builder(ReplyReceiver.KEY_TEXT_REPLY).build()), - intent, - results, - ) - return intent - } - - @Test - fun `reply goes through SendMessageUseCase, marks read, and refreshes the notification in place`() { - val contactKey = "0!12345678" - - ReplyReceiver().onReceive(ApplicationProvider.getApplicationContext(), replyIntent(contactKey, "hello back")) - - verifySuspend { sendMessageUseCase.invoke("hello back", contactKey, null) } - verifySuspend { packetRepository.clearUnreadCount(contactKey, any()) } - // The conversation is re-posted with the sent reply (MessagingStyle confirmation flow), not dismissed. - verifySuspend { notificationManager.refreshConversationAfterReply(contactKey) } - verifySuspend(mode = VerifyMode.exactly(0)) { notificationManager.cancelMessageNotification(any()) } - } - - @Test - fun `notification is cancelled even when the send fails`() { - val contactKey = "0!12345678" - everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } throws RuntimeException("radio down") - - ReplyReceiver().onReceive(ApplicationProvider.getApplicationContext(), replyIntent(contactKey, "hi")) - - verifySuspend(mode = VerifyMode.exactly(0)) { packetRepository.clearUnreadCount(any(), any()) } - verifySuspend { notificationManager.cancelMessageNotification(contactKey) } - } - - @Test - fun `missing RemoteInput results does not send`() { - ReplyReceiver().onReceive(ApplicationProvider.getApplicationContext(), Intent(ReplyReceiver.REPLY_ACTION)) - - verifySuspend(mode = VerifyMode.exactly(0)) { sendMessageUseCase.invoke(any(), any(), any()) } - } -} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidFileService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidFileService.kt index 8924cdcc85..ae1a69c884 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidFileService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidFileService.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.service import android.app.Application import co.touchlab.kermit.Logger import com.eygraber.uri.toAndroidUri +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withContext import okio.BufferedSink import okio.BufferedSource @@ -46,6 +47,8 @@ class AndroidFileService(private val context: Application, private val dispatche FileOutputStream(descriptor.fileDescriptor).sink().buffer().use { sink -> block(sink) } } true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to write to URI: $uri" } false @@ -61,6 +64,8 @@ class AndroidFileService(private val context: Application, private val dispatche true } ?: false success + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to read from URI: $uri" } false diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidLocationService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidLocationService.kt index d28d59fc64..575c75cc22 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidLocationService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidLocationService.kt @@ -16,12 +16,10 @@ */ package org.meshtastic.core.service -import android.Manifest import android.app.Application -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat import kotlinx.coroutines.flow.firstOrNull import org.koin.core.annotation.Single +import org.meshtastic.core.common.hasLocationPermission import org.meshtastic.core.repository.Location import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.LocationService @@ -30,14 +28,9 @@ import org.meshtastic.core.repository.LocationService class AndroidLocationService(private val context: Application, private val locationRepository: LocationRepository) : LocationService { + // The fix becomes the node's fixed position, so an approximate one is refused rather than stored as exact. override suspend fun getCurrentLocation(): Location? { - val hasPermission = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED - - if (!hasPermission) { - return null - } + if (!context.hasLocationPermission(precise = true)) return null return locationRepository.getLocations().firstOrNull() } diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt index 54d13f6e21..09582c8b9a 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt @@ -18,7 +18,6 @@ package org.meshtastic.core.service import android.annotation.SuppressLint import android.app.Application -import androidx.core.location.LocationCompat import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -26,10 +25,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.koin.core.annotation.Single import org.meshtastic.core.common.hasLocationPermission -import org.meshtastic.core.model.Position import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.MeshLocationManager -import kotlin.time.Duration.Companion.milliseconds import org.meshtastic.proto.Position as ProtoPosition @Single @@ -45,29 +42,23 @@ class AndroidMeshLocationManager(private val context: Application, private val l this.sendPositionFn = sendPositionFn if (locationFlow?.isActive == true) return - if (context.hasLocationPermission()) { + // Firmware stores this fix as the node's own position and rebroadcasts it at the channel's precision, which + // it stamps over any precision_bits sent here, so an approximate fix would go out claiming accuracy it lacks. + if (context.hasLocationPermission(precise = true)) { locationFlow = locationRepository .getLocations() .onEach { location -> sendPositionFn( - ProtoPosition.Builder() - .also { wb -> - wb.latitude_i = Position.degI(location.latitude) - wb.longitude_i = Position.degI(location.longitude) - wb.altitude = - if (LocationCompat.hasMslAltitude(location)) { - LocationCompat.getMslAltitudeMeters(location).toInt() - } else { - null - } - wb.altitude_hae = location.altitude.toInt() - wb.time = (location.time.milliseconds.inWholeSeconds).toInt() - wb.ground_speed = location.speed.toInt() - wb.ground_track = location.bearing.toInt() - wb.location_source = ProtoPosition.LocSource.LOC_EXTERNAL - } - .build(), + phonePosition( + latitude = location.latitude, + longitude = location.longitude, + timeMillis = location.timeMillis, + mslAltitudeMeters = location.mslAltitudeMeters, + haeAltitudeMeters = location.altitudeMeters, + speedMetersPerSecond = location.speedMetersPerSecond, + bearingDegrees = location.bearingDegrees, + ), ) } .launchIn(scope) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidNotificationManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidNotificationManager.kt deleted file mode 100644 index c7512e9c7e..0000000000 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidNotificationManager.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.app.NotificationChannel -import android.app.PendingIntent -import android.app.TaskStackBuilder -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.getSystemService -import androidx.core.net.toUri -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.koin.core.annotation.Single -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.NotificationManager -import org.meshtastic.core.resources.R.drawable -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getStringSuspend -import org.meshtastic.core.resources.meshtastic_alerts_notifications -import org.meshtastic.core.resources.meshtastic_low_battery_notifications -import org.meshtastic.core.resources.meshtastic_mesh_beacon_notifications -import org.meshtastic.core.resources.meshtastic_messages_notifications -import org.meshtastic.core.resources.meshtastic_new_nodes_notifications -import org.meshtastic.core.resources.meshtastic_service_notifications -import org.meshtastic.proto.ClientNotification -import android.app.NotificationManager as SystemNotificationManager - -@Single -class AndroidNotificationManager(private val context: Context) : NotificationManager { - - private val notificationManager = - checkNotNull(context.getSystemService()) { "NotificationManager not found" } - - private data class ChannelConfig(val id: String, val importance: Int) - - /** - * Tracks whether notification channels have been created. - * - * Channels are **not** created in the constructor because this singleton is instantiated by Koin during - * [org.meshtastic.core.service.MeshService.onCreate] on the main thread, and channel names come from string - * resources. Instead, channels are lazily ensured before the first [dispatch] call. Note that - * [MeshNotificationManagerImpl.initChannels] already creates a superset of these channels when the orchestrator - * starts, so this lazy path is only a safety net for notifications dispatched before orchestrator initialization. - * - * The mutex is load-bearing: resolving the names suspends, so without it two concurrent [dispatch] calls could both - * pass the flag check and post before the channels exist. - */ - private var channelsInitialized = false - private val channelInitMutex = Mutex() - - private suspend fun ensureChannelsInitialized() = channelInitMutex.withLock { - if (channelsInitialized) return@withLock - channelsInitialized = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channels = - listOf( - createChannel(Notification.Category.Message, Res.string.meshtastic_messages_notifications), - createChannel(Notification.Category.NodeEvent, Res.string.meshtastic_new_nodes_notifications), - createChannel( - Notification.Category.MeshBeacon, - Res.string.meshtastic_mesh_beacon_notifications, - ), - createChannel(Notification.Category.Battery, Res.string.meshtastic_low_battery_notifications), - createChannel(Notification.Category.Alert, Res.string.meshtastic_alerts_notifications), - createChannel(Notification.Category.Service, Res.string.meshtastic_service_notifications), - ) - notificationManager.createNotificationChannels(channels) - notificationManager.removeLegacyCategoryChannels() - } - } - - private suspend fun createChannel( - category: Notification.Category, - nameRes: org.jetbrains.compose.resources.StringResource, - ): NotificationChannel { - val channelConfig = category.channelConfig() - return NotificationChannel(channelConfig.id, getStringSuspend(nameRes), channelConfig.importance) - } - - // Keep category-to-channel mapping aligned with MeshNotificationManagerImpl.NotificationType IDs. - private fun Notification.Category.channelConfig(): ChannelConfig = when (this) { - Notification.Category.Message -> - ChannelConfig( - id = NotificationChannels.MESSAGES, - importance = SystemNotificationManager.IMPORTANCE_HIGH, - ) - - Notification.Category.NodeEvent -> - ChannelConfig( - id = NotificationChannels.NEW_NODES, - importance = SystemNotificationManager.IMPORTANCE_DEFAULT, - ) - - Notification.Category.MeshBeacon -> - ChannelConfig( - id = NotificationChannels.MESH_BEACON, - importance = SystemNotificationManager.IMPORTANCE_LOW, - ) - - Notification.Category.Battery -> - ChannelConfig( - id = NotificationChannels.LOW_BATTERY, - importance = SystemNotificationManager.IMPORTANCE_DEFAULT, - ) - - Notification.Category.Alert -> - ChannelConfig(id = NotificationChannels.ALERTS, importance = SystemNotificationManager.IMPORTANCE_HIGH) - - Notification.Category.Service -> - ChannelConfig(id = NotificationChannels.SERVICE, importance = SystemNotificationManager.IMPORTANCE_MIN) - } - - override suspend fun dispatch(notification: Notification): Boolean = dispatch(notification, onlyAlertOnce = false) - - override fun suppressClientNotificationModal(notification: ClientNotification): Boolean = - notification.isProtectedPositionAdvisory() - - // The advisory's id already comes from ClientNotification.notificationId(), which is stable across repeats (see - // its kdoc) — so it lands in the same tray slot without a dedicated tag or fixed id; only onlyAlertOnce is needed - // to stop it from re-alerting on every update. - override suspend fun dispatchClientNotification( - notification: Notification, - clientNotification: ClientNotification, - ): Boolean = dispatch(notification, onlyAlertOnce = clientNotification.isProtectedPositionAdvisory()) - - private suspend fun dispatch(notification: Notification, onlyAlertOnce: Boolean): Boolean { - ensureChannelsInitialized() - val channelId = notification.category.channelConfig().id - if (!canPostNotifications(channelId)) return false - val id = notification.id ?: notification.hashCode() - val builder = - NotificationCompat.Builder(context, channelId) - .setContentTitle(notification.title) - .setContentText(notification.message) - .setSmallIcon(drawable.meshtastic_ic_notification) - .setAutoCancel(true) - .setSilent(notification.isSilent) - - notification.group?.let { builder.setGroup(it) } - if (onlyAlertOnce) builder.setOnlyAlertOnce(true) - - if (notification.type == Notification.Type.Error) { - builder.setPriority(NotificationCompat.PRIORITY_HIGH) - } - - notification.deepLinkUri?.let { uri -> builder.setContentIntent(createDeepLinkPendingIntent(uri, id)) } - - return try { - notificationManager.notify(id, builder.build()) - true - } catch (_: SecurityException) { - false - } - } - - private fun canPostNotifications(channelId: String): Boolean = - NotificationManagerCompat.from(context).areNotificationsEnabled() && - ( - Build.VERSION.SDK_INT < Build.VERSION_CODES.O || - notificationManager.getNotificationChannel(channelId)?.importance != - SystemNotificationManager.IMPORTANCE_NONE - ) - - /** - * Builds a [PendingIntent] that launches [MainActivity] with the given deep-link URI as [Intent.ACTION_VIEW], so - * the existing deep-link plumbing (`UIViewModel.handleDeepLink` → `DeepLinkRouter` → `MultiBackstack`) can - * synthesize the proper backstack and surface the target screen. - * - * Uses [Class.forName] to avoid pulling the `:androidApp` module into `:core:service` as a Gradle dep. - */ - private fun createDeepLinkPendingIntent(uri: String, requestCode: Int): PendingIntent { - val deepLinkIntent = - Intent(Intent.ACTION_VIEW, uri.toUri(), context, Class.forName(MAIN_ACTIVITY_CLASS)).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - } - return TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(deepLinkIntent) - getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)!! - } - } - - override fun cancel(id: Int) { - notificationManager.cancel(id) - } - - override fun cancelAll() { - notificationManager.cancelAll() - } - - private companion object { - /** - * Fully-qualified name of the host activity that handles `meshtastic://` deep-link intents. Kept as a string to - * avoid creating a module dependency from `:core:service` back onto `:androidApp`. - */ - const val MAIN_ACTIVITY_CLASS = "org.meshtastic.app.MainActivity" - } -} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt index 2c8f2a217b..aed4c42e4a 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt @@ -39,9 +39,8 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.meshtastic.core.common.util.safeCatchingAll import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MeshPrefs -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.boot_reconnect_blocked_message import org.meshtastic.core.resources.boot_reconnect_blocked_title @@ -54,7 +53,7 @@ class BootCompleteReceiver : private val meshPrefs: MeshPrefs by inject() private val dispatchers: CoroutineDispatchers by inject() - private val notificationManager: NotificationManager by inject() + private val serviceNotifications: MeshNotificationManager by inject() private val scope by lazy { CoroutineScope(SupervisorJob() + dispatchers.default) } @Suppress("TooGenericExceptionCaught") @@ -121,8 +120,8 @@ class BootCompleteReceiver : * Posts the one thing the user can act on: a notification naming the missing permission and opening the Connections * screen, where the recovery card now lives. * - * Best-effort by design. If POST_NOTIFICATIONS is also denied the dispatch simply returns false — there is no - * surface left to reach an absent user through, and failing loudly here would help nobody. + * Best-effort by design. If POST_NOTIFICATIONS is also denied the post simply returns false — there is no surface + * left to reach an absent user through, and failing loudly here would help nobody. */ private suspend fun notifyBluetoothPermissionMissing() { // Untranslated fallbacks rather than no notification, and a hard bound on the wait. A boot broadcast runs @@ -133,16 +132,7 @@ class BootCompleteReceiver : @Suppress("TooGenericExceptionCaught") try { - notificationManager.dispatch( - Notification( - title = title, - message = message, - type = Notification.Type.Warning, - category = Notification.Category.Service, - id = BLE_PERMISSION_NOTIFICATION_ID, - deepLinkUri = CONNECTIONS_DEEP_LINK, - ), - ) + serviceNotifications.showReconnectBlockedNotification(title, message) } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -161,11 +151,6 @@ class BootCompleteReceiver : /** A broadcast has seconds, not indefinite time; fall back to untranslated text rather than stall. */ const val STRING_RESOLVE_TIMEOUT_MILLIS = 2_000L - /** Stable id so a second boot replaces the notice rather than stacking another copy. */ - const val BLE_PERMISSION_NOTIFICATION_ID = 0x81E9 - - const val CONNECTIONS_DEEP_LINK = "meshtastic://meshtastic/connections" - const val UNTRANSLATED_BLOCKED_TITLE = "Meshtastic can't reconnect" const val UNTRANSLATED_BLOCKED_MESSAGE = "Nearby devices permission is off, so your radio cannot be reached over Bluetooth. " + diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationActionService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationActionService.kt new file mode 100644 index 0000000000..f4298846f6 --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationActionService.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.core.app.RemoteInput +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.common.util.safeCatching +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioController +import org.meshtastic.core.repository.usecase.SendMessageOutcome +import org.meshtastic.core.repository.usecase.SendMessageUseCase + +/** + * Runs a conversation notification's reply, mark-as-read and thumbs-up actions, which open no UI. + * + * A Service rather than a receiver because Android Auto's notification-messaging contract has these actions handled by + * a Service; the platform allowlists the app to start one from a notification action. A successful action re-posts or + * cancels the conversation, which is the only feedback the phone, a watch or a car gets. A failed reply cancels it so + * the reply field never hangs; a failed thumbs-up has nothing pending and leaves the conversation in the tray. + */ +class ConversationActionService : + Service(), + KoinComponent { + + private val sendMessageUseCase: SendMessageUseCase by inject() + private val packetRepository: PacketRepository by inject() + private val radioController: RadioController by inject() + private val serviceNotifications: MeshNotificationManager by inject() + private val dispatchers: CoroutineDispatchers by inject() + private val scope by lazy { CoroutineScope(SupervisorJob() + dispatchers.io) } + + // stopSelf(startId) is a no-op unless startId is the newest start, so the service only stops once the last + // outstanding action has finished, however the actions interleave. + private var running = 0 + private var newestStartId = 0 + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + synchronized(this) { + running++ + newestStartId = startId + } + scope.launch { + try { + if (intent != null) handle(intent) + } finally { + finishedStartId()?.let(::stopSelf) + } + } + return START_NOT_STICKY + } + + /** The start id to stop with once the last outstanding action is done, or null while others still run. */ + private fun finishedStartId(): Int? = synchronized(this) { + running-- + if (running == 0) newestStartId else null + } + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } + + private suspend fun handle(intent: Intent) { + val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY) ?: return + when (intent.action) { + ACTION_REPLY -> reply(intent, contactKey) + + ACTION_MARK_AS_READ -> { + packetRepository.clearUnreadCount(contactKey, nowMillis) + serviceNotifications.cancelMessageNotification(contactKey) + } + + ACTION_REACT -> react(intent, contactKey) + + else -> Logger.w(tag = TAG) { "Unknown conversation action ${intent.action}" } + } + } + + @Suppress("TooGenericExceptionCaught") // a reply must never crash the service, whatever the radio throws + private suspend fun reply(intent: Intent, contactKey: String) { + val message = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_TEXT_REPLY)?.toString() + if (message.isNullOrBlank()) { + Logger.w(tag = TAG) { "Reply without text for contactKey=$contactKey" } + return + } + try { + // Send first so the reply is never lost to a notification failure. + if (sendMessageUseCase(message, contactKey) == SendMessageOutcome.Refused) { + Logger.w(tag = TAG) { "Reply refused: the conversation is retired" } + safeCatching { serviceNotifications.cancelMessageNotification(contactKey) } + return + } + // Replying reads the conversation; Android Auto keys dismissal off read state, not just cancel(). + packetRepository.clearUnreadCount(contactKey, nowMillis) + // Re-post with the reply appended so the RemoteInput spinner resolves with visible feedback; fall back + // to dismissal so it never hangs. + safeCatching { serviceNotifications.refreshConversationAfterReply(contactKey) } + .onFailure { + Logger.e(tag = TAG, throwable = it) { "Refresh after reply failed" } + safeCatching { serviceNotifications.cancelMessageNotification(contactKey) } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(tag = TAG, throwable = e) { "Reply send failed" } + safeCatching { serviceNotifications.cancelMessageNotification(contactKey) } + } + } + + @Suppress("TooGenericExceptionCaught") + private suspend fun react(intent: Intent, contactKey: String) { + val emoji = intent.getStringExtra(EXTRA_EMOJI) ?: return + val replyId = intent.getIntExtra(EXTRA_REPLY_ID, 0) + try { + radioController.sendReaction(emoji, replyId, contactKey) + safeCatching { serviceNotifications.refreshConversationAfterReply(contactKey) } + .onFailure { Logger.e(tag = TAG, throwable = it) { "Refresh after reaction failed" } } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(tag = TAG, throwable = e) { "Reaction send failed" } + } + } + + companion object { + private const val TAG = "ConversationActionService" + const val ACTION_REPLY = "org.meshtastic.app.REPLY_ACTION" + const val ACTION_MARK_AS_READ = "org.meshtastic.app.MARK_AS_READ" + const val ACTION_REACT = "org.meshtastic.app.REACT_ACTION" + const val EXTRA_CONTACT_KEY = "contact_key" + const val EXTRA_REPLY_ID = "reply_id" + const val EXTRA_EMOJI = "emoji" + const val KEY_TEXT_REPLY = "key_text_reply" + } +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt index 4af8256658..b08bed43ff 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers @@ -44,7 +43,6 @@ import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.unknown_username import org.meshtastic.proto.ChannelSet @@ -133,7 +131,7 @@ class ConversationShortcutPublisher( observeJob = null } - private fun publishShortcuts(conversations: List) { + private suspend fun publishShortcuts(conversations: List) { val limit = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context) // rank == list position, so the most recent conversation is rank 0 and shown first. val shortcuts = @@ -166,28 +164,26 @@ class ConversationShortcutPublisher( } } - private fun buildDmShortcut(dm: Conversation.Dm, rank: Int): ShortcutInfoCompat? { + private suspend fun buildDmShortcut(dm: Conversation.Dm, rank: Int): ShortcutInfoCompat? { val node = nodeRepository.nodeDBbyNum.value.values.find { it.user.id == dm.userId } - // shortLabel is the compact 4-char short name; longLabel the full node name. Fall back to a localized generic - // name when node metadata is missing: the raw contactKey is a user-traceable identifier and must stay confined - // to internal ids and deep-link metadata (privacy-first convention). + // Android Auto and the shade title a conversation by its shortLabel, so both labels carry the full node name + // and the short name goes on the avatar. Fall back to a localized generic name when node metadata is missing: + // the raw contactKey is a user-traceable identifier and must stay confined to internal ids and deep-link + // metadata (privacy-first convention). val shortName = node?.user?.short_name?.takeIf { it.isNotBlank() } val longName = node?.user?.long_name?.takeIf { it.isNotBlank() } - val fallbackName by lazy { getString(Res.string.unknown_username) } - val shortLabel = shortName ?: longName ?: fallbackName - val longLabel = longName ?: shortName ?: fallbackName + val label = longName ?: shortName ?: getStringSuspend(Res.string.unknown_username) // A node-colored pill avatar showing the short name identifies the person and matches the in-app node chip. // Set it on the shortcut itself (not just the Person) so launchers/Android Auto render it instead of a generic // head silhouette. - val icon = - node?.let { - val (foregroundColor, backgroundColor) = nodeColorsFromNum(it.num) - PersonIconFactory.createLabel(shortLabel, backgroundColor, foregroundColor, rounded = false) - } + val icon = node?.let { + val (foregroundColor, backgroundColor) = nodeColorsFromNum(it.num) + PersonIconFactory.createLabel(shortName ?: label, backgroundColor, foregroundColor, rounded = false) + } val person = Person.Builder() - .setName(longLabel) + .setName(label) .setKey(dm.contactKey) // Favorite nodes feed the system's conversation-priority ranking. .setImportant(node?.isFavorite == true) @@ -195,8 +191,8 @@ class ConversationShortcutPublisher( .build() return ShortcutInfoCompat.Builder(context, dm.contactKey) - .setShortLabel(shortLabel) - .setLongLabel(longLabel) + .setShortLabel(label) + .setLongLabel(label) .setRank(rank) .setLocusId(LocusIdCompat(dm.contactKey)) .setPerson(person) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/EncryptedPreferences.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/EncryptedPreferences.kt new file mode 100644 index 0000000000..bf1d2a859f --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/EncryptedPreferences.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.app.Application +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import co.touchlab.kermit.Logger + +/** + * Opens [fileName] as EncryptedSharedPreferences backed by an AES-256-GCM MasterKey (hardware keystore when available), + * or returns null when the keystore cannot be initialized. The key is not gated behind user authentication, so + * background work can read it. + */ +// androidx.security.crypto (MasterKey / EncryptedSharedPreferences) is deprecated by Google with no drop-in AndroidX +// replacement yet. Migrating encrypted storage is a separate, security-sensitive effort; suppress until a stable +// replacement (e.g. Tink) is adopted. +@Suppress("TooGenericExceptionCaught", "DEPRECATION") +internal fun openEncryptedPreferences(app: Application, fileName: String): SharedPreferences? = try { + val masterKey = MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + EncryptedSharedPreferences.create( + app, + fileName, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) +} catch (e: Exception) { + Logger.e(e) { "Failed to initialize encrypted preferences $fileName" } + null +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt index 7711138542..be2d9437c6 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt @@ -18,42 +18,18 @@ package org.meshtastic.core.service import android.app.Application import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import co.touchlab.kermit.Logger import org.koin.core.annotation.Single import org.meshtastic.core.repository.LockdownPassphraseStore import org.meshtastic.core.repository.StoredPassphrase /** - * Encrypted per-device storage for lockdown passphrases. - * - * Uses EncryptedSharedPreferences backed by an AES-256-GCM MasterKey (hardware keystore when available). The key is - * intentionally NOT gated behind biometric authentication so that auto-unlock can run in the background without user - * interaction. + * Encrypted per-device storage for lockdown passphrases, in [openEncryptedPreferences]. The key is not gated behind + * biometric authentication so that auto-unlock can run in the background without user interaction. */ @Single(binds = [LockdownPassphraseStore::class]) class LockdownPassphraseStoreImpl(app: Application) : LockdownPassphraseStore { - // androidx.security.crypto (MasterKey / EncryptedSharedPreferences) is deprecated by Google with no - // drop-in AndroidX replacement yet. Migrating encrypted storage is a separate, security-sensitive - // effort; suppress until a stable replacement (e.g. Tink) is adopted. - @Suppress("TooGenericExceptionCaught", "DEPRECATION") - private val prefs: SharedPreferences? by lazy { - try { - val masterKey = MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - EncryptedSharedPreferences.create( - app, - PREFS_FILE_NAME, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) - } catch (e: Exception) { - Logger.e(e) { "Failed to initialize encrypted passphrase store" } - null - } - } + private val prefs: SharedPreferences? by lazy { openEncryptedPreferences(app, PREFS_FILE_NAME) } private fun requirePrefs(): SharedPreferences = prefs ?: error("Encrypted passphrase store unavailable") diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt deleted file mode 100644 index d78a9822b1..0000000000 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.repository.MeshNotificationManager -import org.meshtastic.core.repository.PacketRepository - -/** A [BroadcastReceiver] that handles "Mark as read" actions from notifications. */ -class MarkAsReadReceiver : - BroadcastReceiver(), - KoinComponent { - - private val packetRepository: PacketRepository by inject() - - private val serviceNotifications: MeshNotificationManager by inject() - - private val dispatchers: CoroutineDispatchers by inject() - - private val scope by lazy { CoroutineScope(dispatchers.io + SupervisorJob()) } - - companion object { - const val MARK_AS_READ_ACTION = "org.meshtastic.app.MARK_AS_READ" - const val CONTACT_KEY = "contact_key" - } - - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == MARK_AS_READ_ACTION) { - val contactKey = intent.getStringExtra(CONTACT_KEY) ?: return - val pendingResult = goAsync() - - scope.launch { - try { - packetRepository.clearUnreadCount(contactKey, nowMillis) - serviceNotifications.cancelMessageNotification(contactKey) - } finally { - pendingResult.finish() - } - } - } - } -} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt index 6faf14418a..c20b018697 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt @@ -17,16 +17,13 @@ package org.meshtastic.core.service import android.app.Notification -import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.TaskStackBuilder -import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE import android.content.Context import android.content.Intent -import android.media.AudioAttributes -import android.media.RingtoneManager import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.content.LocusIdCompat @@ -39,39 +36,47 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.Single import org.meshtastic.core.common.di.ServiceScope import org.meshtastic.core.common.state.RadioOperation import org.meshtastic.core.common.state.RadioOperationLock +import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.model.Channel import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.FirmwareUpdateNotice +import org.meshtastic.core.model.MeshBeaconOffer import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.noiseFloorOrNull -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI +import org.meshtastic.core.repository.FirmwareUpdateProgress +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID +import org.meshtastic.core.repository.notificationId import org.meshtastic.core.resources.R.drawable -import org.meshtastic.core.resources.R.raw import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.channel -import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected import org.meshtastic.core.resources.discovery_scan_in_progress +import org.meshtastic.core.resources.firmware_update_available import org.meshtastic.core.resources.firmware_update_in_progress -import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.firmware_update_notification_android +import org.meshtastic.core.resources.formatDurationSuspend import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.local_stats_bad import org.meshtastic.core.resources.local_stats_battery @@ -88,30 +93,23 @@ import org.meshtastic.core.resources.local_stats_utilization import org.meshtastic.core.resources.low_battery_message import org.meshtastic.core.resources.low_battery_title import org.meshtastic.core.resources.mark_as_read -import org.meshtastic.core.resources.meshtastic_alerts_notifications +import org.meshtastic.core.resources.mesh_beacon_notification_body +import org.meshtastic.core.resources.mesh_beacon_notification_title import org.meshtastic.core.resources.meshtastic_app_name -import org.meshtastic.core.resources.meshtastic_broadcast_notifications -import org.meshtastic.core.resources.meshtastic_low_battery_notifications -import org.meshtastic.core.resources.meshtastic_low_battery_temporary_remote_notifications -import org.meshtastic.core.resources.meshtastic_messages_notifications -import org.meshtastic.core.resources.meshtastic_new_nodes_notifications -import org.meshtastic.core.resources.meshtastic_service_notifications -import org.meshtastic.core.resources.meshtastic_waypoints_notifications -import org.meshtastic.core.resources.new_node_seen import org.meshtastic.core.resources.no_local_stats +import org.meshtastic.core.resources.notification_reaction_to import org.meshtastic.core.resources.powered import org.meshtastic.core.resources.reply import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.you -import org.meshtastic.core.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION -import org.meshtastic.core.service.ReactionReceiver.Companion.REACT_ACTION -import org.meshtastic.core.service.ReplyReceiver.Companion.KEY_TEXT_REPLY import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.LocalStats import org.meshtastic.proto.Telemetry import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import org.meshtastic.core.repository.Notification as MeshNotification /** * Manages the creation and display of all app notifications. @@ -128,6 +126,7 @@ class MeshNotificationManagerImpl( private val conversationShortcutPublisher: Lazy, private val radioConfigRepository: Lazy, private val radioOperationLock: RadioOperationLock, + private val firmwareUpdateStatusRepository: FirmwareUpdateStatusRepository, private val scope: ServiceScope, ) : MeshNotificationManager { @@ -137,9 +136,6 @@ class MeshNotificationManagerImpl( companion object { const val MAX_BATTERY_LEVEL = 100 - // Meshtastic brand accent (Green 500, see .skills/design-standards) — used as the notification accent color - // (small-icon tint) and the notification LED color. - private val NOTIFICATION_COLOR = 0xFF67EA94.toInt() private const val MAX_HISTORY_MESSAGES = 10 private const val MIN_CONTEXT_MESSAGES = 3 private const val SNIPPET_LENGTH = 30 @@ -164,10 +160,18 @@ class MeshNotificationManagerImpl( private const val BUBBLE_DESIRED_HEIGHT_DP = 600 private const val TAG_MESSAGE_SUMMARY = "message_summary" private const val TAG_WAYPOINT = "waypoint" + private const val TAG_REACTION = "reaction" private const val TAG_ALERT = "alert" private const val TAG_NEW_NODE = "new_node" private const val TAG_LOW_BATTERY = "low_battery" private const val TAG_CLIENT = "client" + private const val TAG_MESH_BEACON = "mesh_beacon" + private const val TAG_FIRMWARE_UPDATE = "firmware_update" + private const val TAG_RECONNECT_BLOCKED = "reconnect_blocked" + private const val RECONNECT_BLOCKED_ID = 1 + private val CHANNEL_LABEL_TIMEOUT = 2.seconds + private const val MAIN_ACTIVITY_CLASS = "org.meshtastic.app.MainActivity" + private const val THUMBS_UP = "👍" } private data class ServiceNotificationSnapshot( @@ -177,9 +181,11 @@ class MeshNotificationManagerImpl( val previousMessage: String?, val nextUpdateAt: Long, val activeOperations: Set = emptySet(), + val firmwareProgress: FirmwareUpdateProgress? = null, ) - private data class RenderedServiceNotification(val notification: Notification, val message: String) + /** [message] is the stats text to fall back on later; a firmware-progress render keeps the previous one. */ + private data class RenderedServiceNotification(val notification: Notification, val message: String?) /** * Caches generated avatar icons keyed by (person id + short name + colors) so a conversation rebuild does not @@ -188,10 +194,10 @@ class MeshNotificationManagerImpl( */ private val personIconCache = ConcurrentHashMap() - /** Rounded variant, for surfaces that mask the icon into a circle (notification bubbles). */ - private fun cachedRoundedPersonIcon(key: String, shortName: String, backgroundColor: Int, foregroundColor: Int) = - personIconCache.getOrPut("rounded|$key|$shortName|$backgroundColor|$foregroundColor") { - PersonIconFactory.createLabel(shortName, backgroundColor, foregroundColor, rounded = true) + /** Adaptive variant, for notification bubbles, which the system masks to its own shape. */ + private fun cachedBubblePersonIcon(key: String, shortName: String, backgroundColor: Int, foregroundColor: Int) = + personIconCache.getOrPut("adaptive|$key|$shortName|$backgroundColor|$foregroundColor") { + PersonIconFactory.createAdaptive(shortName, backgroundColor, foregroundColor) } /** Circular, node-colored avatar holding the sender's full short name (e.g. "2c3d"), not just its first letter. */ @@ -200,169 +206,83 @@ class MeshNotificationManagerImpl( PersonIconFactory.createLabel(shortName, backgroundColor, foregroundColor, rounded = false) } - /** - * Sealed class to define the properties of each notification channel. This centralizes channel configuration and - * makes it type-safe. - */ - private sealed class NotificationType( - val channelId: String, - val channelNameRes: StringResource, - val importance: Int, - ) { - object ServiceState : - NotificationType( - NotificationChannels.SERVICE, - Res.string.meshtastic_service_notifications, - NotificationManager.IMPORTANCE_MIN, - ) - - object DirectMessage : - NotificationType( - NotificationChannels.MESSAGES, - Res.string.meshtastic_messages_notifications, - NotificationManager.IMPORTANCE_HIGH, - ) - - object BroadcastMessage : - NotificationType( - NotificationChannels.BROADCASTS, - Res.string.meshtastic_broadcast_notifications, - NotificationManager.IMPORTANCE_DEFAULT, - ) - - object Waypoint : - NotificationType( - NotificationChannels.WAYPOINTS, - Res.string.meshtastic_waypoints_notifications, - NotificationManager.IMPORTANCE_DEFAULT, - ) - - object Alert : - NotificationType( - NotificationChannels.ALERTS, - Res.string.meshtastic_alerts_notifications, - NotificationManager.IMPORTANCE_HIGH, - ) - - object NewNode : - NotificationType( - NotificationChannels.NEW_NODES, - Res.string.meshtastic_new_nodes_notifications, - NotificationManager.IMPORTANCE_DEFAULT, - ) - - object LowBatteryLocal : - NotificationType( - NotificationChannels.LOW_BATTERY, - Res.string.meshtastic_low_battery_notifications, - NotificationManager.IMPORTANCE_DEFAULT, - ) - - object LowBatteryRemote : - NotificationType( - NotificationChannels.LOW_BATTERY_REMOTE, - Res.string.meshtastic_low_battery_temporary_remote_notifications, - NotificationManager.IMPORTANCE_DEFAULT, - ) - - object Client : - NotificationType( - NotificationChannels.CLIENT, - Res.string.client_notification, - NotificationManager.IMPORTANCE_HIGH, - ) - - companion object { - // A list of all types for easy initialization. - fun allTypes() = listOf( - ServiceState, - DirectMessage, - BroadcastMessage, - Waypoint, - Alert, - NewNode, - LowBatteryLocal, - LowBatteryRemote, - Client, - ) - } - } - override fun clearNotifications() { notificationManager.cancelAll() } /** - * Creates all necessary notification channels on devices running Android O or newer. This should be called once - * when the service is created. - * - * Deliberately blocking (Main-thread, one-time cost): the orchestrator posts the foreground-service notification - * synchronously right after this returns, so channels must exist before then — do not lazy-gate this into the - * suspend notify paths. Blocking [getString] is safe here only because Main is not a Dispatchers.Default worker. + * Guarantees the foreground-service channel synchronously, because the orchestrator posts the service notification + * right after this returns, then creates the rest off the calling thread. Every post awaits [ensureChannels]. */ override fun initChannels() { notificationManager.removeLegacyCategoryChannels() - NotificationType.allTypes().forEach { type -> createNotificationChannel(type) } + ensureServiceChannel() + scope.launch { ensureChannels() } } - private fun createNotificationChannel(type: NotificationType) { - if (notificationManager.getNotificationChannel(type.channelId) != null) return + /** Creates the service channel under the app label if it is missing; [ensureChannels] later gives it its name. */ + private fun ensureServiceChannel() { + if (notificationManager.getNotificationChannel(NotificationChannelSpec.Service.id) != null) return + // The platform throws for a channel whose group does not exist yet, and this runs before ensureChannels. + notificationManager.createNotificationChannelGroup( + NotificationChannelSpec.Service.group.toGroup(applicationLabel), + ) + notificationManager.createNotificationChannel( + NotificationChannelSpec.Service.toChannel(context, name = applicationLabel, description = ""), + ) + } - val channelName = getString(type.channelNameRes) - val channel = - NotificationChannel(type.channelId, channelName, type.importance).apply { - lightColor = NOTIFICATION_COLOR - lockscreenVisibility = Notification.VISIBILITY_PUBLIC // Default, can be overridden + private val channelsMutex = Mutex() + private var channelsReady = false - // Type-specific configurations - when (type) { - NotificationType.ServiceState -> { - lockscreenVisibility = Notification.VISIBILITY_PRIVATE - } + private class ChannelLabels( + val groups: Map, + val names: Map, + val descriptions: Map, + ) - NotificationType.DirectMessage, - NotificationType.BroadcastMessage, - NotificationType.Waypoint, - NotificationType.NewNode, - NotificationType.LowBatteryLocal, - NotificationType.LowBatteryRemote, - -> { - setShowBadge(true) - setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(), - ) - if (type == NotificationType.LowBatteryRemote) enableVibration(true) - } + private suspend fun resolveChannelLabels() = ChannelLabels( + groups = NotificationChannelGroupSpec.entries.associateWith { getStringSuspend(it.nameRes) }, + names = NotificationChannelSpec.entries.associateWith { getStringSuspend(it.nameRes) }, + descriptions = NotificationChannelSpec.entries.associateWith { getStringSuspend(it.descriptionRes) }, + ) - NotificationType.Alert -> { - setShowBadge(true) - enableLights(true) - enableVibration(true) - setBypassDnd(true) - val alertSoundUri = - "${SCHEME_ANDROID_RESOURCE}://${context.packageName}/${raw.meshtastic_alert}".toUri() - setSound( - alertSoundUri, - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ALARM) // More appropriate for an alert - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(), - ) - } - - NotificationType.Client -> { - setShowBadge(true) - } - } + /** + * Creates every channel group and channel, once per process. Re-creating an existing channel only refreshes its + * name, description and (if it had none) group, so this also carries a locale change and the grouping onto installs + * whose channels predate them. + * + * Label loading is bounded: a boot broadcast posts through here with seconds to live, and a channel under the app + * label beats no notification. Such a pass does not count as done, so the next post puts the real labels on. + */ + internal suspend fun ensureChannels() = channelsMutex.withLock { + if (channelsReady) return@withLock + val labels = withTimeoutOrNull(CHANNEL_LABEL_TIMEOUT) { safeCatching { resolveChannelLabels() }.getOrNull() } + val groups = + NotificationChannelGroupSpec.entries.map { it.toGroup(labels?.groups?.get(it) ?: applicationLabel) } + notificationManager.createNotificationChannelGroups(groups) + val channels = + NotificationChannelSpec.entries.map { spec -> + spec.toChannel( + context, + name = labels?.names?.get(spec) ?: applicationLabel, + description = labels?.descriptions?.get(spec).orEmpty(), + ) } - notificationManager.createNotificationChannel(channel) + notificationManager.createNotificationChannels(channels) + channelsReady = labels != null } + /** Whether a post on [spec] would reach the user: app notifications on, and the channel not blocked. */ + private fun canPost(spec: NotificationChannelSpec): Boolean = + NotificationManagerCompat.from(context).areNotificationsEnabled() && + notificationManager.getNotificationChannel(spec.id)?.importance != NotificationManager.IMPORTANCE_NONE + private val serviceNotificationLock = Any() + private val lowBatteryLock = Any() + + /** Per node, how many times its low-battery warning was cancelled; guarded by [lowBatteryLock]. */ + private val lowBatteryCancellations = mutableMapOf() private val applicationLabel: String by lazy { context.applicationInfo.loadLabel(context.packageManager).toString().ifBlank { context.packageName } } @@ -386,6 +306,14 @@ class MeshNotificationManagerImpl( } } } + scope.launch { + firmwareUpdateStatusRepository.progress.collect { progress -> + synchronized(serviceNotificationLock) { + val last = serviceNotificationSnapshots.replayCache.lastOrNull() ?: return@synchronized + serviceNotificationSnapshots.tryEmit(last.copy(firmwareProgress = progress)) + } + } + } } /** @@ -394,6 +322,7 @@ class MeshNotificationManagerImpl( * Multiplatform resource loading. */ fun getServiceNotification(): Notification { + ensureServiceChannel() val cached = synchronized(serviceNotificationLock) { cachedServiceNotification } return cached ?: createServiceStateNotification(name = applicationLabel, message = null, nextUpdateAt = 0) } @@ -468,10 +397,20 @@ class MeshNotificationManagerImpl( previousMessage = cachedMessage, nextUpdateAt = nextStatsUpdateMillis, activeOperations = radioOperationLock.activeOperations, + firmwareProgress = firmwareUpdateStatusRepository.progress.value, ) } private suspend fun renderServiceNotification(snapshot: ServiceNotificationSnapshot): RenderedServiceNotification { + snapshot.firmwareProgress?.let { progress -> + val notification = + createFirmwareProgressNotification( + title = getStringSuspend(Res.string.firmware_update_in_progress), + text = progress.message.resolve(), + percent = progress.percent, + ) + return RenderedServiceNotification(notification, message = snapshot.previousMessage) + } // A held operation outranks the connection state. During a firmware update the device is deliberately // deselected, so the state alone would report "Disconnected" over a flash that is running perfectly. val title = @@ -522,7 +461,16 @@ class MeshNotificationManagerImpl( channelName: String?, isSilent: Boolean, ) { - showConversationNotification(contactKey, isBroadcast, channelName, conversationName = name, isSilent = isSilent) + ensureChannels() + val builder = + commonBuilder(NotificationChannelSpec.Reactions, createOpenMessageIntent(contactKey)) + .setContentTitle(name) + .setContentText(emoji) + .setCategory(Notification.CATEGORY_MESSAGE) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setAutoCancel(true) + if (isSilent) builder.setSilent(true) + notificationManager.notify(TAG_REACTION, contactKey.hashCode(), builder.build()) } override suspend fun updateWaypointNotification( @@ -532,6 +480,7 @@ class MeshNotificationManagerImpl( waypointId: Int, isSilent: Boolean, ) { + ensureChannels() val notification = createWaypointNotification(name, message, waypointId, isSilent) notificationManager.notify(TAG_WAYPOINT, contactKey.hashCode(), notification) } @@ -543,6 +492,7 @@ class MeshNotificationManagerImpl( conversationName: String, isSilent: Boolean = false, ) { + ensureChannels() // Publish (or refresh) a long-lived conversation shortcut before the notification references it, so Android can // rank the notification in the shade's Conversations section and expose it to Android Auto/Wear. A channel name // labels a broadcast conversation; a direct message is labelled by the other participant's name. @@ -606,48 +556,36 @@ class MeshNotificationManagerImpl( return } - val ourNode = nodeRepository.value.ourNodeInfo.value - val meName = ourNode?.user?.long_name ?: getStringSuspend(Res.string.you) - val me = - Person.Builder() - .setName(meName) - .setKey(ourNode?.user?.id ?: NodeAddress.ID_LOCAL) - .apply { - ourNode?.let { - setIcon(cachedPersonIcon(it.user.id, it.user.short_name, it.colors.second, it.colors.first)) - } - } - .build() - - val messagingStyle = - NotificationCompat.MessagingStyle(me) - .setGroupConversation(true) - .setConversationTitle(getStringSuspend(Res.string.meshtastic_app_name)) - - activeNotifications.forEach { sbn -> - // Prefer the child's real MessagingStyle: its latest message carries the actual sender (Person, icon) and - // timestamp, so the summary line reads "Hawk Ridge: …" rather than the conversation title. + // InboxStyle, not MessagingStyle: the summary has no reply or mark-as-read actions, and Android Auto takes a + // MessagingStyle notification for a conversation it can answer. + val you = getStringSuspend(Res.string.you) + val lines = activeNotifications.mapNotNull { sbn -> + // Prefer the child's real MessagingStyle: its latest message carries the actual sender, so the line + // reads "Hawk Ridge: …" rather than the conversation title. val latest = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(sbn.notification) ?.messages ?.lastOrNull() - if (latest?.text != null) { - val senderPerson = latest.person ?: Person.Builder().setName(getStringSuspend(Res.string.you)).build() - messagingStyle.addMessage(latest.text, latest.timestamp, senderPerson) + val sender = latest?.person?.name ?: latest?.let { you } + val text = latest?.text + if (sender != null && text != null) { + "$sender: $text" } else { - // Fallback for children without an extractable style: rebuild a generic line from the extras. val senderTitle = sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE) val messageText = sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT) - if (senderTitle != null && messageText != null) { - messagingStyle.addMessage(messageText, sbn.postTime, Person.Builder().setName(senderTitle).build()) - } + if (senderTitle != null && messageText != null) "$senderTitle: $messageText" else null } } + val appName = getStringSuspend(Res.string.meshtastic_app_name) + val inboxStyle = NotificationCompat.InboxStyle().setBigContentTitle(appName) + lines.forEach { inboxStyle.addLine(it) } val summaryNotification = - commonBuilder(NotificationType.DirectMessage) + commonBuilder(NotificationChannelSpec.DirectMessages) .setSmallIcon(drawable.meshtastic_ic_notification) - .setStyle(messagingStyle) + .setContentTitle(appName) + .setContentText(lines.lastOrNull()) + .setStyle(inboxStyle) .setGroup(GROUP_KEY_MESSAGES) .setGroupSummary(true) // Only the child conversation notifications alert; without this the summary (posted on a HIGH channel) @@ -659,31 +597,131 @@ class MeshNotificationManagerImpl( notificationManager.notify(TAG_MESSAGE_SUMMARY, SUMMARY_ID, summaryNotification) } - override fun showAlertNotification(contactKey: String, name: String, alert: String) { - val notification = createAlertNotification(contactKey, name, alert) - // Use a consistent, unique ID for each alert source. - notificationManager.notify(TAG_ALERT, name.hashCode(), notification) + override suspend fun showAlertNotification(contactKey: String, name: String, alert: String) { + ensureChannels() + notificationManager.notify(TAG_ALERT, contactKey.hashCode(), createAlertNotification(contactKey, name, alert)) } - override fun showNewNodeSeenNotification(node: Node) { - val notification = createNewNodeSeenNotification(node.user.short_name, node.user.long_name, node.num) - notificationManager.notify(TAG_NEW_NODE, node.num, notification) - } - - override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) { - val notification = createLowBatteryNotification(node, isRemote) - notificationManager.notify(TAG_LOW_BATTERY, node.num, notification) - } - - override fun showClientNotification(clientNotification: ClientNotification) { + override suspend fun showMeshBeaconNotification(offer: MeshBeaconOffer) { + ensureChannels() + val title = getStringSuspend(Res.string.mesh_beacon_notification_title) + val message = offer.message.ifBlank { getStringSuspend(Res.string.mesh_beacon_notification_body) } val notification = - createClientNotification(getString(Res.string.client_notification), clientNotification.message) - notificationManager.notify(TAG_CLIENT, clientNotification.toString().hashCode(), notification) + commonBuilder(NotificationChannelSpec.MeshBeacon, createDeepLinkIntent("discovery", offer.fromNodeNum)) + .setCategory(Notification.CATEGORY_RECOMMENDATION) + .setAutoCancel(true) + .setContentTitle(title) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .build() + notificationManager.notify(TAG_MESH_BEACON, offer.fromNodeNum, notification) + } + + override suspend fun showNewNodeSeenNotification(node: Node, title: String) { + ensureChannels() + notificationManager.notify(TAG_NEW_NODE, node.num, createNewNodeSeenNotification(title, node)) + } + + override fun cancelNewNodeNotification(nodeNum: Int) = notificationManager.cancel(TAG_NEW_NODE, nodeNum) + + override suspend fun showLowBatteryNotification(node: Node, isRemote: Boolean) = + postLowBattery(node, isRemote, onlyIfShowing = false) + + override suspend fun updateLowBatteryNotification(node: Node, isRemote: Boolean) = + postLowBattery(node, isRemote, onlyIfShowing = true) + + /** + * Recovery cancels from separate work, so a post that was already resolving its text when the battery recovered + * must not land afterwards: it notes the node's cancellation count before building and posts only if it is + * unchanged. + */ + private suspend fun postLowBattery(node: Node, isRemote: Boolean, onlyIfShowing: Boolean) { + val cancellationsBefore = synchronized(lowBatteryLock) { lowBatteryCancellations[node.num] ?: 0 } + ensureChannels() + val notification = createLowBatteryNotification(node, isRemote) + beforeLowBatteryPost?.invoke() + synchronized(lowBatteryLock) { + if ((lowBatteryCancellations[node.num] ?: 0) != cancellationsBefore) return + val showing = notificationManager.activeNotifications.any { it.tag == TAG_LOW_BATTERY && it.id == node.num } + if (!onlyIfShowing || showing) notificationManager.notify(TAG_LOW_BATTERY, node.num, notification) + } + } + + /** Test seam run between building a low-battery notification and posting it, to race a recovery in. */ + internal var beforeLowBatteryPost: (() -> Unit)? = null + + override suspend fun showClientNotification( + clientNotification: ClientNotification, + title: String, + severity: MeshNotification.Type, + ) { + ensureChannels() + val message = clientNotification.message + val warning = severity == MeshNotification.Type.Warning || severity == MeshNotification.Type.Error + val notification = + commonBuilder(NotificationChannelSpec.Client) + .setCategory(if (warning) Notification.CATEGORY_ERROR else Notification.CATEGORY_STATUS) + .setAutoCancel(true) + .setContentTitle(title) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + // The firmware repeats this advisory on every position request; the tray slot is stable across + // repeats (see notificationId), so only the first one should sound. + .setOnlyAlertOnce(clientNotification.isProtectedPositionAdvisory()) + .build() + notificationManager.notify(TAG_CLIENT, clientNotification.notificationId(), notification) + } + + override fun clearClientNotification(clientNotification: ClientNotification) = + notificationManager.cancel(TAG_CLIENT, clientNotification.notificationId()) + + override fun suppressClientNotificationModal(clientNotification: ClientNotification): Boolean = + clientNotification.isProtectedPositionAdvisory() + + override suspend fun showFirmwareUpdateNotification(notice: FirmwareUpdateNotice): Boolean { + ensureChannels() + if (!canPost(NotificationChannelSpec.DeviceStatus)) return false + val message = + getStringSuspend( + Res.string.firmware_update_notification_android, + notice.currentVersion, + notice.stableVersion, + ) + val id = notice.notificationKey.hashCode() + val notification = + commonBuilder(NotificationChannelSpec.DeviceStatus, createDeepLinkIntent("firmware/update", id)) + .setCategory(Notification.CATEGORY_RECOMMENDATION) + .setAutoCancel(true) + .setContentTitle(getStringSuspend(Res.string.firmware_update_available)) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .build() + notificationManager.notify(TAG_FIRMWARE_UPDATE, id, notification) + return true + } + + override suspend fun showReconnectBlockedNotification(title: String, message: String): Boolean { + ensureChannels() + if (!canPost(NotificationChannelSpec.DeviceStatus)) return false + val notification = + commonBuilder( + NotificationChannelSpec.DeviceStatus, + createDeepLinkIntent("connections", RECONNECT_BLOCKED_ID), + ) + .setCategory(Notification.CATEGORY_ERROR) + .setAutoCancel(true) + .setContentTitle(title) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .build() + notificationManager.notify(TAG_RECONNECT_BLOCKED, RECONNECT_BLOCKED_ID, notification) + return true } override suspend fun cancelMessageNotification(contactKey: String) { val id = contactKey.hashCode() notificationManager.cancel(TAG_MESSAGE, id) + notificationManager.cancel(TAG_REACTION, id) // Rebuild (or clear) the group summary so it doesn't keep showing the dismissed conversation in Android Auto. // Pass the id we just cancelled so a stale activeNotifications snapshot doesn't keep the summary alive. showGroupSummary(justCancelledId = id) @@ -716,18 +754,17 @@ class MeshNotificationManagerImpl( showConversationNotification(contactKey, isBroadcast, channelName, conversationName, isSilent = true) } - override fun cancelLowBatteryNotification(node: Node) = notificationManager.cancel(TAG_LOW_BATTERY, node.num) - - override fun clearClientNotification(notification: ClientNotification) = - notificationManager.cancel(TAG_CLIENT, notification.toString().hashCode()) + override fun cancelLowBatteryNotification(node: Node) = synchronized(lowBatteryLock) { + lowBatteryCancellations[node.num] = (lowBatteryCancellations[node.num] ?: 0) + 1 + notificationManager.cancel(TAG_LOW_BATTERY, node.num) + } // endregion // region Notification Creation private fun createServiceStateNotification(name: String, message: String?, nextUpdateAt: Long?): Notification { val builder = - commonBuilder(NotificationType.ServiceState) - .setPriority(NotificationCompat.PRIORITY_MIN) + commonBuilder(NotificationChannelSpec.Service) .setCategory(Notification.CATEGORY_SERVICE) .setOngoing(true) // Android 12+ may defer FGS notifications ~10s; show immediately so the user watching a @@ -753,6 +790,27 @@ class MeshNotificationManagerImpl( return builder.build() } + /** + * A flash is a start-to-end journey the user may background, so the service notification asks to be promoted to a + * Live Update while one runs. The platform grants it only on a channel above MIN and with + * POST_PROMOTED_NOTIFICATIONS; elsewhere this is an ordinary ongoing progress notification. + */ + private fun createFirmwareProgressNotification(title: String, text: String, percent: Int?): Notification { + val style = NotificationCompat.ProgressStyle().setProgressIndeterminate(percent == null) + percent?.let { style.setProgress(it) } + return commonBuilder(NotificationChannelSpec.Service) + .setCategory(Notification.CATEGORY_PROGRESS) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .setContentTitle(title) + .setContentText(text) + .setStyle(style) + .setRequestPromotedOngoing(true) + .apply { percent?.let { setShortCriticalText(MetricFormatter.percent(it)) } } + .build() + } + @Suppress("LongMethod") private suspend fun createConversationNotification( contactKey: String, @@ -761,7 +819,7 @@ class MeshNotificationManagerImpl( history: List, isSilent: Boolean = false, ): Notification { - val type = if (isBroadcast) NotificationType.BroadcastMessage else NotificationType.DirectMessage + val type = if (isBroadcast) NotificationChannelSpec.Broadcasts else NotificationChannelSpec.DirectMessages val builder = commonBuilder(type, createOpenMessageIntent(contactKey)) if (isSilent) { @@ -813,7 +871,7 @@ class MeshNotificationManagerImpl( val text = msg.originalMessage?.let { original -> - "↩️ \"${original.node.user.short_name}: ${original.text.take(SNIPPET_LENGTH)}...\": ${msg.text}" + "↩️ \"${original.node.user.short_name}: ${snippet(original.text)}\": ${msg.text}" } ?: msg.text if (msg.read && (anyUnread || index != lastIndex)) { @@ -839,17 +897,18 @@ class MeshNotificationManagerImpl( ) .build() style.addMessage( - "${reaction.emoji} to \"${msg.text.take(SNIPPET_LENGTH)}...\"", + getStringSuspend(Res.string.notification_reaction_to, reaction.emoji, snippet(msg.text)), reaction.timestamp, reactor, ) } } val lastMessage = history.last() - // The bubble wears the other party's avatar, not ours — a bubble is recognised by who is in it. - val bubbleNode = lastMessage.node + // The bubble wears the other party's avatar, not ours — a bubble is recognised by who is in it. After a reply + // the newest message is our own, so look back for the newest one someone else sent. + val bubbleNode = history.lastOrNull { !it.fromLocal }?.node ?: lastMessage.node val bubbleIcon = - cachedRoundedPersonIcon( + cachedBubblePersonIcon( bubbleNode.user.id, bubbleNode.user.short_name, bubbleNode.colors.second, @@ -871,6 +930,19 @@ class MeshNotificationManagerImpl( .build(), ) .setBubbleMetadata(createBubbleMetadata(contactKey, bubbleIcon)) + .apply { + // Android Auto shows the large icon as the conversation image; a channel keeps the car's default. + if (!isBroadcast) { + val avatar = + cachedPersonIcon( + bubbleNode.user.id, + bubbleNode.user.short_name, + bubbleNode.colors.second, + bubbleNode.colors.first, + ) + setLargeIcon(avatar.toIcon(context)) + } + } .setAutoCancel(true) .setStyle(style) .setGroup(GROUP_KEY_MESSAGES) @@ -879,14 +951,7 @@ class MeshNotificationManagerImpl( .setShowWhen(true) .addAction(createReplyAction(contactKey)) .addAction(createMarkAsReadAction(contactKey)) - .addAction( - createReactionAction( - contactKey = contactKey, - packetId = lastMessage.packetId, - toId = lastMessage.node.user.id, - channelIndex = lastMessage.node.channel, - ), - ) + .addAction(createReactionAction(contactKey = contactKey, packetId = lastMessage.packetId)) return builder.build() } @@ -897,15 +962,17 @@ class MeshNotificationManagerImpl( waypointId: Int, isSilent: Boolean, ): Notification { - val person = Person.Builder().setName(name).build() - val style = NotificationCompat.MessagingStyle(person).addMessage(message, nowMillis, person) - + // Not MessagingStyle: only a conversation that can be replied to may look like one to Android Auto. val builder = - commonBuilder(NotificationType.Waypoint, createOpenWaypointIntent(waypointId)) - .setCategory(Notification.CATEGORY_MESSAGE) + commonBuilder( + NotificationChannelSpec.Waypoints, + createDeepLinkIntent("map?waypointId=$waypointId", waypointId), + ) + .setCategory(Notification.CATEGORY_STATUS) .setAutoCancel(true) - .setStyle(style) - .setGroup(GROUP_KEY_MESSAGES) + .setContentTitle(name) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .setWhen(nowMillis) .setShowWhen(true) @@ -917,42 +984,38 @@ class MeshNotificationManagerImpl( return builder.build() } - private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification { - val person = Person.Builder().setName(name).build() - val style = NotificationCompat.MessagingStyle(person).addMessage(alert, nowMillis, person) - - return commonBuilder(NotificationType.Alert, createOpenMessageIntent(contactKey)) - .setPriority(NotificationCompat.PRIORITY_HIGH) + private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification = + commonBuilder(NotificationChannelSpec.Alerts, createOpenMessageIntent(contactKey)) .setCategory(Notification.CATEGORY_ALARM) .setAutoCancel(true) - .setStyle(style) + .setContentTitle(name) + .setContentText(alert) + .setStyle(NotificationCompat.BigTextStyle().bigText(alert)) + .build() + + private fun createNewNodeSeenNotification(title: String, node: Node): Notification { + val message = node.user.long_name + return commonBuilder(NotificationChannelSpec.NewNodes, createOpenNodeDetailIntent(node.num)) + .setCategory(Notification.CATEGORY_STATUS) + .setAutoCancel(true) + .setContentTitle(title) + .setWhen(nowMillis) + .setShowWhen(true) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) .build() } - private fun createNewNodeSeenNotification(name: String, message: String, nodeNum: Int): Notification { - val title = getString(Res.string.new_node_seen, name) - val builder = - commonBuilder(NotificationType.NewNode, createOpenNodeDetailIntent(nodeNum)) - .setCategory(Notification.CATEGORY_STATUS) - .setAutoCancel(true) - .setContentTitle(title) - .setWhen(nowMillis) - .setShowWhen(true) - .setContentText(message) - .setStyle(NotificationCompat.BigTextStyle().bigText(message)) - - return builder.build() - } - - private fun createLowBatteryNotification(node: Node, isRemote: Boolean): Notification { - val type = if (isRemote) NotificationType.LowBatteryRemote else NotificationType.LowBatteryLocal - val title = getString(Res.string.low_battery_title, node.user.short_name) + private suspend fun createLowBatteryNotification(node: Node, isRemote: Boolean): Notification { + val type = if (isRemote) NotificationChannelSpec.LowBatteryRemote else NotificationChannelSpec.LowBattery + val title = getStringSuspend(Res.string.low_battery_title, node.user.short_name) val batteryLevel = node.deviceMetrics.battery_level ?: 0 - val message = getString(Res.string.low_battery_message, node.user.long_name, batteryLevel) + val message = getStringSuspend(Res.string.low_battery_message, node.user.long_name, batteryLevel) + // Not ongoing: an ongoing notification never bridges to a watch, and recovery cancels this one anyway. return commonBuilder(type, createOpenNodeDetailIntent(node.num)) .setCategory(Notification.CATEGORY_STATUS) - .setOngoing(true) + .setAutoCancel(true) .setOnlyAlertOnce(true) .setProgress(MAX_BATTERY_LEVEL, batteryLevel, false) .setContentTitle(title) @@ -963,39 +1026,38 @@ class MeshNotificationManagerImpl( .build() } - private fun createClientNotification(name: String, message: String): Notification = - commonBuilder(NotificationType.Client) - .setCategory(Notification.CATEGORY_ERROR) - .setAutoCancel(true) - .setContentTitle(name) - .setContentText(message) - .setStyle(NotificationCompat.BigTextStyle().bigText(message)) - .build() - // endregion // region Helper/Builder Methods private val openAppIntent: PendingIntent by lazy { val intent = - Intent(context, Class.forName("org.meshtastic.app.MainActivity")).apply { + Intent(context, Class.forName(MAIN_ACTIVITY_CLASS)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP } PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } - private fun createOpenMessageIntent(contactKey: String): PendingIntent { - val deepLinkUri = "$DEEP_LINK_BASE_URI/messages/$contactKey".toUri() + /** + * Opens `meshtastic://meshtastic/[path]` in MainActivity, whose deep-link router synthesizes the backstack. Each + * link is its own PendingIntent because the URI takes part in intent equality; [requestCode] only has to be stable. + */ + private fun createDeepLinkIntent(path: String, requestCode: Int): PendingIntent { val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - } - + Intent(Intent.ACTION_VIEW, "$DEEP_LINK_BASE_URI/$path".toUri(), context, Class.forName(MAIN_ACTIVITY_CLASS)) + .apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } return TaskStackBuilder.create(context).run { addNextIntentWithParentStack(deepLinkIntent) - getPendingIntent(contactKey.hashCode(), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + checkNotNull( + getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT), + ) } } + private fun createOpenMessageIntent(contactKey: String) = + createDeepLinkIntent("messages/$contactKey", contactKey.hashCode()) + + private fun createOpenNodeDetailIntent(nodeNum: Int) = createDeepLinkIntent("nodes/$nodeNum", nodeNum) + /** * Bubble target: [org.meshtastic.app.BubbleActivity], not the launcher activity. A bubble's activity must be * resizeable, embeddable and document-launched, and making the launcher activity document-launched would change how @@ -1027,46 +1089,30 @@ class MeshNotificationManagerImpl( .build() } - private fun createOpenWaypointIntent(waypointId: Int): PendingIntent { - val deepLinkUri = "$DEEP_LINK_BASE_URI/map?waypointId=$waypointId".toUri() - val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + /** + * An explicit intent for [ConversationActionService]. The conversation rides in the data URI, which takes part in + * PendingIntent identity, so two conversations never share one even if their request codes collide. + */ + private fun conversationActionIntent(action: String, contactKey: String, packetId: Int? = null): Intent { + val data = + "$DEEP_LINK_BASE_URI/messages/$contactKey".toUri().buildUpon().apply { + packetId?.let { appendQueryParameter("packet", it.toString()) } } - - return TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(deepLinkIntent) - getPendingIntent(waypointId, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) - } - } - - private fun createOpenNodeDetailIntent(nodeNum: Int): PendingIntent { - val deepLinkUri = "$DEEP_LINK_BASE_URI/nodes/$nodeNum".toUri() - val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - } - - return TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(deepLinkIntent) - getPendingIntent(nodeNum, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) - } + return Intent(action, data.build(), context, ConversationActionService::class.java) + .putExtra(ConversationActionService.EXTRA_CONTACT_KEY, contactKey) } private suspend fun createReplyAction(contactKey: String): NotificationCompat.Action { val replyLabel = getStringSuspend(Res.string.reply) - val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() + val remoteInput = RemoteInput.Builder(ConversationActionService.KEY_TEXT_REPLY).setLabel(replyLabel).build() - val replyIntent = - Intent(context, ReplyReceiver::class.java).apply { - action = ReplyReceiver.REPLY_ACTION - putExtra(ReplyReceiver.CONTACT_KEY, contactKey) - } + // Mutable so Android Auto and Wear can fill in the RemoteInput text; the intent is explicit, which a mutable + // PendingIntent must be. val replyPendingIntent = - PendingIntent.getBroadcast( + PendingIntent.getService( context, contactKey.hashCode(), - replyIntent, + conversationActionIntent(ConversationActionService.ACTION_REPLY, contactKey), PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) @@ -1075,21 +1121,18 @@ class MeshNotificationManagerImpl( // Required for Android Auto to drive reply hands-free without opening any UI. .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) + // Offers Smart Reply suggestions when the notification is bridged to a Wear OS watch. + .setAllowGeneratedReplies(true) .build() } private suspend fun createMarkAsReadAction(contactKey: String): NotificationCompat.Action { val label = getStringSuspend(Res.string.mark_as_read) - val intent = - Intent(context, MarkAsReadReceiver::class.java).apply { - action = MARK_AS_READ_ACTION - putExtra(MarkAsReadReceiver.CONTACT_KEY, contactKey) - } val pendingIntent = - PendingIntent.getBroadcast( + PendingIntent.getService( context, contactKey.hashCode(), - intent, + conversationActionIntent(ConversationActionService.ACTION_MARK_AS_READ, contactKey), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) @@ -1100,40 +1143,36 @@ class MeshNotificationManagerImpl( .build() } - private fun createReactionAction( - contactKey: String, - packetId: Int, - toId: String, - channelIndex: Int, - ): NotificationCompat.Action { - val label = "👍" + private fun createReactionAction(contactKey: String, packetId: Int): NotificationCompat.Action { + val label = THUMBS_UP val intent = - Intent(context, ReactionReceiver::class.java).apply { - action = REACT_ACTION - putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey) - putExtra(ReactionReceiver.EXTRA_REPLY_ID, packetId) - putExtra(ReactionReceiver.EXTRA_TO_ID, toId) - putExtra(ReactionReceiver.EXTRA_CHANNEL_INDEX, channelIndex) - putExtra(ReactionReceiver.EXTRA_EMOJI, "👍") - } + conversationActionIntent(ConversationActionService.ACTION_REACT, contactKey, packetId) + .putExtra(ConversationActionService.EXTRA_REPLY_ID, packetId) + .putExtra(ConversationActionService.EXTRA_EMOJI, THUMBS_UP) val pendingIntent = - PendingIntent.getBroadcast( + PendingIntent.getService( context, packetId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) - return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_add, label, pendingIntent).build() + return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_add, label, pendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_THUMBS_UP) + .setShowsUserInterface(false) + .build() } + private fun snippet(text: String): String = + if (text.length > SNIPPET_LENGTH) text.take(SNIPPET_LENGTH).trimEnd() + "…" else text + private fun commonBuilder( - type: NotificationType, + type: NotificationChannelSpec, contentIntent: PendingIntent? = null, ): NotificationCompat.Builder { val smallIcon = drawable.meshtastic_ic_notification - return NotificationCompat.Builder(context, type.channelId) + return NotificationCompat.Builder(context, type.id) .setSmallIcon(smallIcon) .setColor(NOTIFICATION_COLOR) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) @@ -1154,7 +1193,8 @@ class MeshNotificationManagerImpl( } } parts.add(BULLET + getStringSuspend(Res.string.local_stats_nodes, num_online_nodes, num_total_nodes)) - parts.add(BULLET + getStringSuspend(Res.string.local_stats_uptime, formatUptime(uptime_seconds))) + val uptime = formatDurationSuspend(uptime_seconds.toLong()) + parts.add(BULLET + getStringSuspend(Res.string.local_stats_uptime, uptime)) parts.add( BULLET + getStringSuspend( @@ -1205,7 +1245,9 @@ class MeshNotificationManagerImpl( private suspend fun DeviceMetrics.formatToStringSuspend(): String { val parts = mutableListOf() battery_level?.let { parts.add(BULLET + getStringSuspend(Res.string.local_stats_battery, it)) } - uptime_seconds?.let { parts.add(BULLET + getStringSuspend(Res.string.local_stats_uptime, formatUptime(it))) } + uptime_seconds?.let { + parts.add(BULLET + getStringSuspend(Res.string.local_stats_uptime, formatDurationSuspend(it.toLong()))) + } if (channel_utilization != null || air_util_tx != null) { parts.add( BULLET + diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt index 580fd93055..7cdc5909c5 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt @@ -42,6 +42,7 @@ import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioTransportFactory /** * Android foreground service that hosts the Meshtastic mesh radio connection. @@ -54,6 +55,8 @@ class MeshService : Service() { private val radioInterfaceService: RadioInterfaceService by inject() + private val transportFactory: RadioTransportFactory by inject() + private val connectionManager: MeshConnectionManager by inject() private val notifications: MeshNotificationManager by inject() @@ -153,7 +156,7 @@ class MeshService : Service() { // while-in-use restricted there) upgrades to connectedDevice|location the next time the user opens the app. val foregroundServiceType = ForegroundStartPolicy.foregroundServiceType( - hasLocationPermission = hasLocationPermission(), + hasLocationPermission = hasLocationPermission(precise = true), appInForeground = isAppInForeground(), ) @@ -170,7 +173,7 @@ class MeshService : Service() { } val address = radioInterfaceService.getDeviceAddress() - if (isValidDeviceAddress(address)) { + if (isValidDeviceAddress(address) && transportFactory.isAddressValid(address)) { // Address is already loaded and valid — proceed normally. addressWaitJob?.cancel() addressWaitJob = null @@ -205,7 +208,9 @@ class MeshService : Service() { withTimeoutOrNull(DEVICE_ADDRESS_SETTLE_MS) { radioInterfaceService.currentDeviceAddressFlow.first(::isValidDeviceAddress) } - when (serviceStayAliveDecision(resolved, radioOperationLock.isActive)) { + when ( + serviceStayAliveDecision(resolved, radioOperationLock.isActive, transportFactory::isAddressValid) + ) { ServiceStayAliveDecision.STAY_FOR_DEVICE -> { Logger.i { "MeshService: selected device resolved (${resolved.anonymize}) after address-flow wait" @@ -217,7 +222,7 @@ class MeshService : Service() { Logger.i { "MeshService: no device selected, but a radio operation is in flight; staying up" } ServiceStayAliveDecision.STOP -> { - Logger.i { "MeshService: no device selected after address flow settled; stopping" } + Logger.i { "MeshService: no connectable device selected after address flow settled; stopping" } stopServiceCleanly() } } diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannelSpec.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannelSpec.kt new file mode 100644 index 0000000000..66702f729c --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannelSpec.kt @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE +import android.content.Context +import android.media.AudioAttributes +import android.media.RingtoneManager +import android.net.Uri +import androidx.core.net.toUri +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.resources.R.raw +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.device +import org.meshtastic.core.resources.meshtastic_alerts_notifications +import org.meshtastic.core.resources.meshtastic_alerts_notifications_description +import org.meshtastic.core.resources.meshtastic_broadcast_notifications +import org.meshtastic.core.resources.meshtastic_broadcast_notifications_description +import org.meshtastic.core.resources.meshtastic_client_notifications +import org.meshtastic.core.resources.meshtastic_client_notifications_description +import org.meshtastic.core.resources.meshtastic_device_status_notifications +import org.meshtastic.core.resources.meshtastic_device_status_notifications_description +import org.meshtastic.core.resources.meshtastic_low_battery_notifications +import org.meshtastic.core.resources.meshtastic_low_battery_notifications_description +import org.meshtastic.core.resources.meshtastic_low_battery_temporary_remote_notifications +import org.meshtastic.core.resources.meshtastic_low_battery_temporary_remote_notifications_description +import org.meshtastic.core.resources.meshtastic_mesh_beacon_notifications +import org.meshtastic.core.resources.meshtastic_mesh_beacon_notifications_description +import org.meshtastic.core.resources.meshtastic_messages_notifications +import org.meshtastic.core.resources.meshtastic_messages_notifications_description +import org.meshtastic.core.resources.meshtastic_new_nodes_notifications +import org.meshtastic.core.resources.meshtastic_new_nodes_notifications_description +import org.meshtastic.core.resources.meshtastic_reactions_notifications +import org.meshtastic.core.resources.meshtastic_reactions_notifications_description +import org.meshtastic.core.resources.meshtastic_service_notifications +import org.meshtastic.core.resources.meshtastic_service_notifications_description +import org.meshtastic.core.resources.meshtastic_waypoints_notifications +import org.meshtastic.core.resources.meshtastic_waypoints_notifications_description +import org.meshtastic.core.resources.messages +import org.meshtastic.core.resources.notification_group_mesh + +/** Meshtastic brand accent (Green 500, see .skills/design-standards): the small-icon tint and the LED colour. */ +internal val NOTIFICATION_COLOR = 0xFF67EA94.toInt() + +/** + * Every notification channel the app owns. The platform lets an app change a channel's name, description and (once) + * group after creation; importance, sound, vibration and lights are fixed the first time the channel is created, so an + * importance here must never drop below what an earlier release created for the same id. + */ +internal enum class NotificationChannelSpec( + val id: String, + val nameRes: StringResource, + val descriptionRes: StringResource, + val group: NotificationChannelGroupSpec, + val importance: Int, +) { + // A foreground-service channel below LOW is raised to LOW by the platform on first use anyway. + Service( + NotificationChannels.SERVICE, + Res.string.meshtastic_service_notifications, + Res.string.meshtastic_service_notifications_description, + NotificationChannelGroupSpec.Device, + NotificationManager.IMPORTANCE_LOW, + ), + DirectMessages( + NotificationChannels.MESSAGES, + Res.string.meshtastic_messages_notifications, + Res.string.meshtastic_messages_notifications_description, + NotificationChannelGroupSpec.Messages, + NotificationManager.IMPORTANCE_HIGH, + ), + Broadcasts( + NotificationChannels.BROADCASTS, + Res.string.meshtastic_broadcast_notifications, + Res.string.meshtastic_broadcast_notifications_description, + NotificationChannelGroupSpec.Messages, + NotificationManager.IMPORTANCE_DEFAULT, + ), + Waypoints( + NotificationChannels.WAYPOINTS, + Res.string.meshtastic_waypoints_notifications, + Res.string.meshtastic_waypoints_notifications_description, + NotificationChannelGroupSpec.Messages, + NotificationManager.IMPORTANCE_DEFAULT, + ), + Reactions( + NotificationChannels.REACTIONS, + Res.string.meshtastic_reactions_notifications, + Res.string.meshtastic_reactions_notifications_description, + NotificationChannelGroupSpec.Messages, + NotificationManager.IMPORTANCE_DEFAULT, + ), + Alerts( + NotificationChannels.ALERTS, + Res.string.meshtastic_alerts_notifications, + Res.string.meshtastic_alerts_notifications_description, + NotificationChannelGroupSpec.Messages, + NotificationManager.IMPORTANCE_HIGH, + ), + NewNodes( + NotificationChannels.NEW_NODES, + Res.string.meshtastic_new_nodes_notifications, + Res.string.meshtastic_new_nodes_notifications_description, + NotificationChannelGroupSpec.Mesh, + NotificationManager.IMPORTANCE_DEFAULT, + ), + MeshBeacon( + NotificationChannels.MESH_BEACON, + Res.string.meshtastic_mesh_beacon_notifications, + Res.string.meshtastic_mesh_beacon_notifications_description, + NotificationChannelGroupSpec.Mesh, + NotificationManager.IMPORTANCE_LOW, + ), + LowBatteryRemote( + NotificationChannels.LOW_BATTERY_REMOTE, + Res.string.meshtastic_low_battery_temporary_remote_notifications, + Res.string.meshtastic_low_battery_temporary_remote_notifications_description, + NotificationChannelGroupSpec.Mesh, + NotificationManager.IMPORTANCE_DEFAULT, + ), + LowBattery( + NotificationChannels.LOW_BATTERY, + Res.string.meshtastic_low_battery_notifications, + Res.string.meshtastic_low_battery_notifications_description, + NotificationChannelGroupSpec.Device, + NotificationManager.IMPORTANCE_DEFAULT, + ), + Client( + NotificationChannels.CLIENT, + Res.string.meshtastic_client_notifications, + Res.string.meshtastic_client_notifications_description, + NotificationChannelGroupSpec.Device, + NotificationManager.IMPORTANCE_HIGH, + ), + DeviceStatus( + NotificationChannels.DEVICE_STATUS, + Res.string.meshtastic_device_status_notifications, + Res.string.meshtastic_device_status_notifications_description, + NotificationChannelGroupSpec.Device, + NotificationManager.IMPORTANCE_DEFAULT, + ), + ; + + fun toChannel(context: Context, name: String, description: String): NotificationChannel = + NotificationChannel(id, name, importance).also { channel -> + channel.description = description + channel.group = group.id + channel.lightColor = NOTIFICATION_COLOR + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + channel.setShowBadge(true) + when (this) { + Service -> { + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + channel.setShowBadge(false) + } + + Alerts -> { + channel.enableLights(true) + channel.enableVibration(true) + val alertSound = "$SCHEME_ANDROID_RESOURCE://${context.packageName}/${raw.meshtastic_alert}".toUri() + channel.setSound(alertSound, soundAttributes(AudioAttributes.USAGE_ALARM)) + } + + LowBatteryRemote -> { + channel.enableVibration(true) + channel.setSound(defaultSound, soundAttributes(AudioAttributes.USAGE_NOTIFICATION)) + } + + DirectMessages, + Broadcasts, + Waypoints, + Reactions, + NewNodes, + LowBattery, + DeviceStatus, + -> channel.setSound(defaultSound, soundAttributes(AudioAttributes.USAGE_NOTIFICATION)) + + MeshBeacon, + Client, + -> Unit + } + } + + private companion object { + val defaultSound: Uri? = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + fun soundAttributes(usage: Int): AudioAttributes = + AudioAttributes.Builder().setUsage(usage).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build() + } +} + +internal enum class NotificationChannelGroupSpec(val id: String, val nameRes: StringResource) { + Messages("group_messages", Res.string.messages), + Mesh("group_mesh", Res.string.notification_group_mesh), + Device("group_device", Res.string.device), + ; + + fun toGroup(name: String): NotificationChannelGroup = NotificationChannelGroup(id, name) +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannels.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannels.kt index bf1057865e..d99da7fae4 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannels.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/NotificationChannels.kt @@ -22,11 +22,13 @@ object NotificationChannels { const val BROADCASTS = "my_broadcasts" const val WAYPOINTS = "my_waypoints" const val ALERTS = "my_alerts" + const val REACTIONS = "my_reactions" const val NEW_NODES = "new_nodes" const val MESH_BEACON = "mesh_beacon" const val LOW_BATTERY = "low_battery" const val LOW_BATTERY_REMOTE = "low_battery_remote" const val CLIENT = "client_notifications" + const val DEVICE_STATUS = "device_status" // Legacy enum-name channel IDs introduced by alpha channel routing. val LEGACY_CATEGORY_IDS = listOf("Message", "NodeEvent", "Battery", "Alert", "Service") diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/PersonIconFactory.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/PersonIconFactory.kt index 5ba4c42dc9..54cae4ecd4 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/PersonIconFactory.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/PersonIconFactory.kt @@ -33,6 +33,7 @@ import androidx.core.graphics.drawable.IconCompat internal object PersonIconFactory { private const val ICON_SIZE = 128 + private const val ADAPTIVE_ICON_SIZE = ICON_SIZE * 108 / 72 private const val TEXT_SIZE_RATIO = 0.5f // Leave a margin so multi-character labels (e.g. a 4-char node short name) don't touch the edge. @@ -58,9 +59,37 @@ internal object PersonIconFactory { fun createLabel(label: String, backgroundColor: Int, foregroundColor: Int, rounded: Boolean): IconCompat = render(label.ifBlank { "?" }, backgroundColor, foregroundColor, rounded) + /** + * Full-bleed adaptive avatar showing [label], for notification bubbles: Android 10 rejects a plain bitmap bubble + * icon, and the system applies its own mask shape. + */ + fun createAdaptive(label: String, backgroundColor: Int, foregroundColor: Int): IconCompat { + val bitmap = createBitmap(ADAPTIVE_ICON_SIZE, ADAPTIVE_ICON_SIZE) + val canvas = Canvas(bitmap) + canvas.drawColor(backgroundColor) + // The system shows only the central 72/108 of an adaptive bitmap, so the label is fitted to that safe zone. + drawLabel(canvas, label.ifBlank { "?" }, foregroundColor, contentSize = ICON_SIZE.toFloat()) + return IconCompat.createWithAdaptiveBitmap(bitmap) + } + private fun firstInitial(name: String): String = if (name.isEmpty()) "?" else String(Character.toChars(name.codePointAt(0))).uppercase() + private fun drawLabel(canvas: Canvas, text: String, foregroundColor: Int, contentSize: Float) { + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + paint.color = foregroundColor + paint.textAlign = Paint.Align.CENTER + paint.textSize = contentSize * TEXT_SIZE_RATIO + // Shrink the text if it would overflow the icon (keeps 4-char short names inside the shape). + val measured = paint.measureText(text) + val maxWidth = contentSize * MAX_TEXT_WIDTH_RATIO + if (measured > maxWidth) paint.textSize *= maxWidth / measured + + val xPos = canvas.width / 2f + val yPos = canvas.height / 2f - (paint.descent() + paint.ascent()) / 2f + canvas.drawText(text, xPos, yPos, paint) + } + private fun render(text: String, backgroundColor: Int, foregroundColor: Int, rounded: Boolean): IconCompat { val bitmap = createBitmap(ICON_SIZE, ICON_SIZE) val canvas = Canvas(bitmap) @@ -79,17 +108,7 @@ internal object PersonIconFactory { canvas.drawRoundRect(0f, top, size, size - top, cap, cap, paint) } - paint.color = foregroundColor - paint.textAlign = Paint.Align.CENTER - paint.textSize = ICON_SIZE * TEXT_SIZE_RATIO - // Shrink the text if it would overflow the icon (keeps 4-char short names inside the circle). - val measured = paint.measureText(text) - val maxWidth = ICON_SIZE * MAX_TEXT_WIDTH_RATIO - if (measured > maxWidth) paint.textSize *= maxWidth / measured - - val xPos = canvas.width / 2f - val yPos = canvas.height / 2f - (paint.descent() + paint.ascent()) / 2f - canvas.drawText(text, xPos, yPos, paint) + drawLabel(canvas, text, foregroundColor, contentSize = size) return IconCompat.createWithBitmap(bitmap) } diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt deleted file mode 100644 index 433fdb7e0f..0000000000 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.repository.RadioController -import kotlin.coroutines.cancellation.CancellationException - -/** - * Handles inline emoji reaction actions from message notifications. - * - * Uses [goAsync] to keep the process alive while the coroutine dispatches the reaction through [RadioController], - * matching the pattern used by [ReplyReceiver] and [MarkAsReadReceiver]. - */ -class ReactionReceiver : - BroadcastReceiver(), - KoinComponent { - - private val radioController: RadioController by inject() - - private val dispatchers: CoroutineDispatchers by inject() - - private val scope by lazy { CoroutineScope(SupervisorJob() + dispatchers.io) } - - @Suppress("TooGenericExceptionCaught", "ReturnCount") - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != REACT_ACTION) return - - val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY) ?: return - val reaction = intent.getStringExtra(EXTRA_EMOJI) ?: intent.getStringExtra(EXTRA_REACTION) ?: return - val replyId = intent.getIntExtra(EXTRA_REPLY_ID, intent.getIntExtra(EXTRA_PACKET_ID, 0)) - - val pendingResult = goAsync() - scope.launch { - try { - radioController.sendReaction(reaction, replyId, contactKey) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.e(e) { "Error sending reaction" } - } finally { - pendingResult.finish() - } - } - } - - companion object { - const val REACT_ACTION = "org.meshtastic.app.REACT_ACTION" - const val EXTRA_CONTACT_KEY = "extra_contact_key" - const val EXTRA_REACTION = "extra_reaction" - const val EXTRA_REPLY_ID = "extra_reply_id" - const val EXTRA_PACKET_ID = "extra_packet_id" - const val EXTRA_TO_ID = "extra_to_id" - const val EXTRA_CHANNEL_INDEX = "extra_channel_index" - const val EXTRA_EMOJI = "extra_emoji" - } -} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt deleted file mode 100644 index a1ade2f75a..0000000000 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.app.RemoteInput -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.repository.MeshNotificationManager -import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.usecase.SendMessageUseCase - -/** - * A [BroadcastReceiver] that handles inline replies from notifications. - * - * This receiver is triggered when a user replies to a message directly from a notification. It extracts the reply text - * and the contact key from the intent, sends the message through [SendMessageUseCase] — so notification replies get the - * same pipeline as in-app sends (history save, durable queue, transforms) — and then refreshes the conversation - * notification so the sent reply appears in place (falling back to dismissal if the send or refresh fails). - */ -class ReplyReceiver : - BroadcastReceiver(), - KoinComponent { - private val sendMessageUseCase: SendMessageUseCase by inject() - - private val meshServiceNotifications: MeshNotificationManager by inject() - - private val packetRepository: PacketRepository by inject() - - private val dispatchers: CoroutineDispatchers by inject() - - private val scope by lazy { CoroutineScope(dispatchers.io + SupervisorJob()) } - - companion object { - private const val TAG = "ReplyReceiver" - const val REPLY_ACTION = "org.meshtastic.app.REPLY_ACTION" - const val CONTACT_KEY = "contactKey" - const val KEY_TEXT_REPLY = "key_text_reply" - } - - @Suppress("TooGenericExceptionCaught") // a reply must never crash the receiver, whatever the radio throws - override fun onReceive(context: Context, intent: Intent) { - val remoteInput = RemoteInput.getResultsFromIntent(intent) - if (remoteInput == null) { - Logger.w(tag = TAG) { "reply received but RemoteInput was null" } - return - } - - val contactKey = intent.getStringExtra(CONTACT_KEY).orEmpty() - val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString().orEmpty() - Logger.i(tag = TAG) { "reply for contactKey=$contactKey len=${message.length}" } - - val pendingResult: PendingResult? = goAsync() - scope.launch { - try { - // Send first so the reply isn't lost; the notification update can't break the send. - sendMessageUseCase(message, contactKey) - // Replying implies the conversation has been read — mark it so, like the mark-as-read action. - // Android Auto keys notification dismissal off read state, not just cancel(). - packetRepository.clearUnreadCount(contactKey, nowMillis) - Logger.i(tag = TAG) { "reply sent + marked read" } - // Re-post the conversation silently with the sent reply appended — the MessagingStyle confirmation - // flow. This resolves the RemoteInput spinner with visible feedback instead of the notification - // vanishing. Fall back to dismissal so the spinner never hangs if the refresh itself fails. - safeCatching { meshServiceNotifications.refreshConversationAfterReply(contactKey) } - .onFailure { - Logger.e(tag = TAG, throwable = it) { "refresh after reply failed" } - safeCatching { meshServiceNotifications.cancelMessageNotification(contactKey) } - } - } catch (e: CancellationException) { - // Preserve structured concurrency — never treat cancellation as a failed send. - throw e - } catch (e: Exception) { - Logger.e(tag = TAG, throwable = e) { "reply send failed" } - // The send failed; dismiss so the RemoteInput spinner resolves rather than hanging forever. - safeCatching { meshServiceNotifications.cancelMessageNotification(contactKey) } - .onFailure { Logger.e(tag = TAG, throwable = it) { "cancel notification failed" } } - } finally { - pendingResult?.finish() - } - } - } -} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt index 1f12c25bd1..492033359b 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt @@ -18,41 +18,18 @@ package org.meshtastic.core.service import android.app.Application import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import co.touchlab.kermit.Logger import org.koin.core.annotation.Single import org.meshtastic.core.repository.SecurityKeyBackupStore import org.meshtastic.core.repository.StoredSecurityKeys /** - * Encrypted per-node storage for security key backups. - * - * Uses EncryptedSharedPreferences backed by an AES-256-GCM MasterKey (hardware keystore when available), mirroring + * Encrypted per-node storage for security key backups, in [openEncryptedPreferences] like * [LockdownPassphraseStoreImpl]. Keyed by node number, matching iOS's per-node Keychain entries. */ @Single(binds = [SecurityKeyBackupStore::class]) class SecurityKeyBackupStoreImpl(app: Application) : SecurityKeyBackupStore { - // androidx.security.crypto (MasterKey / EncryptedSharedPreferences) is deprecated by Google with no - // drop-in AndroidX replacement yet. Migrating encrypted storage is a separate, security-sensitive - // effort; suppress until a stable replacement (e.g. Tink) is adopted. - @Suppress("TooGenericExceptionCaught", "DEPRECATION") - private val prefs: SharedPreferences? by lazy { - try { - val masterKey = MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - EncryptedSharedPreferences.create( - app, - PREFS_FILE_NAME, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) - } catch (e: Exception) { - Logger.e(e) { "Failed to initialize encrypted security key backup store" } - null - } - } + private val prefs: SharedPreferences? by lazy { openEncryptedPreferences(app, PREFS_FILE_NAME) } @Suppress("ReturnCount") override fun get(nodeNum: Int): StoredSecurityKeys? { diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/StartMeshServiceOnAddressChange.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/StartMeshServiceOnAddressChange.kt new file mode 100644 index 0000000000..5bb3641ab7 --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/StartMeshServiceOnAddressChange.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import android.content.Context +import org.koin.core.annotation.Single + +/** Starts [MeshService] for a newly selected device, since the foreground service owns the connection on Android. */ +@Single +class StartMeshServiceOnAddressChange(private val context: Context) : DeviceAddressChangeHook { + override fun onDeviceAddressChanged() { + MeshService.startService(context, ServiceStartTrigger.DeviceAddressChanged) + } +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt index 356c312c28..f5104739c2 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt @@ -16,85 +16,9 @@ */ package org.meshtastic.core.service.di -import android.content.Context import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module -import org.koin.core.annotation.Single -import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.common.di.ServiceScope -import org.meshtastic.core.repository.AdminController -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.MeshDataHandler -import org.meshtastic.core.repository.MeshLocationManager -import org.meshtastic.core.repository.MeshMessageProcessor -import org.meshtastic.core.repository.MeshPrefs -import org.meshtastic.core.repository.MessagingController -import org.meshtastic.core.repository.NodeController -import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.NotificationManager -import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.PlatformAnalytics -import org.meshtastic.core.repository.QueryController -import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.RadioController -import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.repository.UiPrefs -import org.meshtastic.core.service.MeshService -import org.meshtastic.core.service.RadioControllerImpl -import org.meshtastic.core.service.ServiceStartTrigger -import org.meshtastic.core.service.startService @Module @ComponentScan("org.meshtastic.core.service") -class CoreServiceAndroidModule { - @Suppress("LongParameterList") - @Single( - binds = - [ - RadioController::class, - AdminController::class, - MessagingController::class, - NodeController::class, - QueryController::class, - ], - ) - fun radioController( - context: Context, - serviceRepository: ServiceRepository, - nodeRepository: NodeRepository, - commandSender: CommandSender, - nodeManager: NodeManager, - radioInterfaceService: RadioInterfaceService, - locationManager: MeshLocationManager, - packetRepository: Lazy, - dataHandler: Lazy, - analytics: PlatformAnalytics, - meshPrefs: MeshPrefs, - uiPrefs: UiPrefs, - databaseManager: DatabaseManager, - notificationManager: NotificationManager, - messageProcessor: Lazy, - radioConfigRepository: RadioConfigRepository, - scope: ServiceScope, - ): RadioController = RadioControllerImpl( - serviceRepository = serviceRepository, - nodeRepository = nodeRepository, - commandSender = commandSender, - nodeManager = nodeManager, - radioInterfaceService = radioInterfaceService, - locationManager = locationManager, - packetRepository = packetRepository, - dataHandler = dataHandler, - analytics = analytics, - meshPrefs = meshPrefs, - uiPrefs = uiPrefs, - databaseManager = databaseManager, - notificationManager = notificationManager, - messageProcessor = messageProcessor, - radioConfigRepository = radioConfigRepository, - scope = scope, - onDeviceAddressChanged = { MeshService.startService(context, ServiceStartTrigger.DeviceAddressChanged) }, - ) -} +class CoreServiceAndroidModule diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt index d458c9ee56..f8352be31d 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt @@ -22,32 +22,18 @@ import androidx.work.WorkerParameters import co.touchlab.kermit.Logger import kotlinx.coroutines.CancellationException import org.koin.android.annotation.KoinWorker -import org.meshtastic.core.repository.MeshLogPrefs -import org.meshtastic.core.repository.MeshLogRepository -import org.meshtastic.core.repository.MeshLogRetention +import org.meshtastic.core.service.MeshLogCleanup @KoinWorker class MeshLogCleanupWorker( appContext: Context, workerParams: WorkerParameters, - private val meshLogRepository: MeshLogRepository, - private val meshLogPrefs: MeshLogPrefs, + private val meshLogCleanup: MeshLogCleanup, ) : CoroutineWorker(appContext, workerParams) { @Suppress("TooGenericExceptionCaught") override suspend fun doWork(): Result = try { - val policy = meshLogPrefs.awaitCleanupPolicy() - val retentionDays = policy.retentionDays - val retentionWindow = MeshLogRetention.windowOrNull(retentionDays) - if (!policy.loggingEnabled) { - logger.i { "Skipping cleanup because mesh log storage is disabled" } - } else if (retentionWindow == null) { - logger.i { "Skipping cleanup because retention is set to never delete" } - } else { - logger.d { "Cleaning logs older than $retentionWindow" } - meshLogRepository.deleteLogsOlderThan(retentionDays) - logger.i { "Successfully cleaned old MeshLog entries" } - } + meshLogCleanup.runOnce() Result.success() } catch (e: CancellationException) { throw e diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt index 3d592bcb9d..86489deb4a 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt @@ -36,7 +36,8 @@ class SendMessageWorker( private val radioController: RadioController, ) : CoroutineWorker(context, params) { - @Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount") + // A failed or cancelled send rolls its claim back (NonCancellable in the repository); cancellation is rethrown. + @Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount", "SuspendFunSwallowedCancellation") override suspend fun doWork(): Result { val packetUuid = inputData.getLong(KEY_PACKET_UUID, 0L) val myNodeNum = inputData.getInt(KEY_MY_NODE_NUM, 0) diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt index d3f780bc5f..883d0d76ce 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt @@ -282,8 +282,8 @@ internal class AdminControllerImpl( } } - override suspend fun rebootToDfu(nodeNum: Int) { - commandSender.sendAdmin(nodeNum) { + override suspend fun rebootToDfu(nodeNum: Int, packetId: Int) { + commandSender.sendAdmin(nodeNum, packetId) { AdminMessage.Builder().also { wb -> wb.enter_dfu_mode_request = true }.build() } } @@ -323,6 +323,7 @@ internal class AdminControllerImpl( // ── Edit Settings (transactional) ─────────────────────────────────────── + @Suppress("SuspendFunSwallowedCancellation") // a block's cancellation is held only until the commit, then rethrown override suspend fun editSettings(destNum: Int, block: suspend AdminEditScope.() -> Unit) { val isLocalDestination = destNum == nodeManager.myNodeNum.value requireBeginBoundaryAccepted(destNum) @@ -439,7 +440,8 @@ internal class AdminControllerImpl( projections.forEach { projection -> applyProjection(projection) } } - @Suppress("TooGenericExceptionCaught") + // Only called under applyStagedProjections' NonCancellable, so no cancellation of its own reaches the catch. + @Suppress("TooGenericExceptionCaught", "SuspendFunSwallowedCancellation") private suspend fun applyProjection(projection: suspend () -> Unit) { try { projection() diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/BootReconnectDecision.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/BootReconnectDecision.kt index ce9b4716c8..dbc2d95391 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/BootReconnectDecision.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/BootReconnectDecision.kt @@ -17,9 +17,8 @@ package org.meshtastic.core.service import org.meshtastic.core.common.util.isValidDeviceAddress - -/** Prefix the app uses for a persisted BLE device address. TCP is `t`, USB is `s`. */ -private const val BLE_ADDRESS_PREFIX = 'x' +import org.meshtastic.core.model.DeviceAddress +import org.meshtastic.core.model.InterfaceId /** What a boot / package-replaced broadcast should do about the previously selected device. */ enum class BootReconnectDecision { @@ -53,7 +52,7 @@ enum class BootReconnectDecision { fun bootReconnectDecision(address: String?, hasBluetoothPermission: Boolean): BootReconnectDecision = when { !isValidDeviceAddress(address) -> BootReconnectDecision.NO_DEVICE - address?.firstOrNull() == BLE_ADDRESS_PREFIX && !hasBluetoothPermission -> + DeviceAddress.parse(address)?.interfaceId == InterfaceId.BLUETOOTH && !hasBluetoothPermission -> BootReconnectDecision.BLE_PERMISSION_MISSING else -> BootReconnectDecision.START_SERVICE diff --git a/core/repository/src/androidMain/kotlin/org/meshtastic/core/repository/Location.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DeviceAddressChangeHook.kt similarity index 77% rename from core/repository/src/androidMain/kotlin/org/meshtastic/core/repository/Location.kt rename to core/service/src/commonMain/kotlin/org/meshtastic/core/service/DeviceAddressChangeHook.kt index 1288849757..5699adf916 100644 --- a/core/repository/src/androidMain/kotlin/org/meshtastic/core/repository/Location.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DeviceAddressChangeHook.kt @@ -14,7 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.repository +package org.meshtastic.core.service -/** Android-specific location object typealias for KMP. */ -actual typealias Location = android.location.Location +/** Platform work to run after [RadioControllerImpl] commits a new device address. */ +fun interface DeviceAddressChangeHook { + fun onDeviceAddressChanged() +} diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshLogCleanup.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshLogCleanup.kt new file mode 100644 index 0000000000..1973fa11a2 --- /dev/null +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshLogCleanup.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.delay +import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.safeCatching +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.core.repository.MeshLogRetention +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +/** + * Prunes the mesh log to the persisted retention policy. Android runs [runOnce] from `MeshLogCleanupWorker` on + * WorkManager's hourly schedule; desktop has no WorkManager and runs [runHourly] for as long as the app is open. + */ +@Single +class MeshLogCleanup(private val meshLogRepository: MeshLogRepository, private val meshLogPrefs: MeshLogPrefs) { + + /** One pass under the persisted policy. Failures propagate so each scheduler reports them its own way. */ + suspend fun runOnce() { + val policy = meshLogPrefs.awaitCleanupPolicy() + val retentionWindow = MeshLogRetention.windowOrNull(policy.retentionDays) + if (!policy.loggingEnabled) { + logger.i { "Skipping cleanup because mesh log storage is disabled" } + } else if (retentionWindow == null) { + logger.i { "Skipping cleanup because retention is set to never delete" } + } else { + logger.d { "Cleaning logs older than $retentionWindow" } + meshLogRepository.deleteLogsOlderThan(policy.retentionDays) + logger.i { "Successfully cleaned old MeshLog entries" } + } + } + + /** + * Runs [runOnce] [FIRST_RUN_DELAY] after it is called and every [INTERVAL] after that, until the calling coroutine + * is cancelled. A failed pass is logged and the next one still runs, as WorkManager runs the next period after a + * failed one. + */ + suspend fun runHourly(): Nothing { + delay(FIRST_RUN_DELAY) + while (true) { + safeCatching { runOnce() }.onFailure { logger.e(it) { "Failed to clean MeshLog entries" } } + delay(INTERVAL) + } + } + + companion object { + /** Keeps the first pass clear of the startup database switch and radio handshake. */ + val FIRST_RUN_DELAY = 1.minutes + + val INTERVAL = 1.hours + + private val logger = Logger.withTag("MeshLogCleanup") + } +} diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt index 5a6d225896..362ee6729f 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt @@ -34,6 +34,7 @@ import org.meshtastic.core.common.util.isValidDeviceAddress import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.common.util.safeCatchingAll import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshMessageProcessor @@ -112,7 +113,7 @@ class MeshServiceOrchestrator( // later successful manual connect with no recovery layer for the life of the process. The OS enforces the // permission at the socket, so a genuinely local connect fails exactly as it would have; the difference is // the user now has an explanation and the fix in hand. - if (address?.firstOrNull() == InterfaceId.TCP.id && !localNetworkAccess.isGranted()) { + if (DeviceAddress.parse(address)?.interfaceId == InterfaceId.TCP && !localNetworkAccess.isGranted()) { Logger.w { "Local network access not granted; the persisted TCP reconnect may time out" } serviceStateWriter.setErrorMessage( // Same shape as ScannerViewModel's scan-failure messages: resource lookup can fail outside a @@ -230,7 +231,7 @@ class MeshServiceOrchestrator( // coroutine is fire-and-forget; typical runtime is ~100-150ms which comfortably fits // inside Android's onDestroy() grace window. CoroutineScope(SupervisorJob() + dispatchers.default).launch { - runCatching { radioInterfaceService.disconnect() } + safeCatching { radioInterfaceService.disconnect() } } scopeRef.getAndSet(null)?.cancel() } diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/PhonePosition.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/PhonePosition.kt new file mode 100644 index 0000000000..86bcffb1a8 --- /dev/null +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/PhonePosition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import org.meshtastic.core.model.Position +import org.meshtastic.core.model.util.GeoConstants +import org.meshtastic.core.model.util.UnitConversions +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds +import org.meshtastic.proto.Position as ProtoPosition + +/** + * The position the phone reports for its node. A reading the fix lacks stays unset rather than going out as 0, and + * speed and track use the units firmware's own GPS sends: km/h and 1e-5 degrees. + */ +internal fun phonePosition( + latitude: Double, + longitude: Double, + timeMillis: Long, + mslAltitudeMeters: Double?, + haeAltitudeMeters: Double?, + speedMetersPerSecond: Float?, + bearingDegrees: Float?, +): ProtoPosition = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = Position.degI(latitude) + wb.longitude_i = Position.degI(longitude) + wb.altitude = mslAltitudeMeters?.toInt() + wb.altitude_hae = haeAltitudeMeters?.toInt() + wb.time = timeMillis.milliseconds.inWholeSeconds.toInt() + wb.ground_speed = speedMetersPerSecond?.let { UnitConversions.metersPerSecondToKph(it).roundToInt() } + wb.ground_track = bearingDegrees?.let { (it / GeoConstants.HEADING_DEG).roundToInt() } + wb.location_source = ProtoPosition.LocSource.LOC_EXTERNAL + } + .build() diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt index 96425ef4b7..2137b71279 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt @@ -46,7 +46,7 @@ internal class QueryControllerImpl( override suspend fun requestPosition(destNum: Int, currentPosition: Position) { if (destNum == nodeManager.myNodeNum.value) return val provideLocation = uiPrefs.shouldProvideNodeLocation(myNodeNum).value - // Position(0.0, 0.0, 0) is the protocol-level "no position" sentinel. + // Position(0.0, 0.0, 0) means "attach no coordinates"; the sender leaves them off the wire. val resolvedPosition = if (provideLocation) { currentPosition.takeIf { it.isValid() } diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt index 413d0f43bc..12dae40d1b 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt @@ -18,7 +18,6 @@ package org.meshtastic.core.service import co.touchlab.kermit.Logger import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -30,7 +29,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.common.di.ServiceScope import org.meshtastic.core.model.ConnectionEpochs import org.meshtastic.core.model.ConnectionLifecycle import org.meshtastic.core.model.ConnectionState @@ -41,12 +42,12 @@ import org.meshtastic.core.repository.ConnectionIdentity import org.meshtastic.core.repository.MeshDataHandler import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.MessagingController import org.meshtastic.core.repository.NodeController import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.QueryController @@ -96,6 +97,16 @@ internal suspend fun restoreLocalConfigurationIfOwned( * surfacing, packet-id generation, location provisioning, and device-address switching. */ @Suppress("LongParameterList") +@Single( + binds = + [ + RadioController::class, + AdminController::class, + MessagingController::class, + NodeController::class, + QueryController::class, + ], +) class RadioControllerImpl( private val serviceRepository: ServiceRepository, nodeRepository: NodeRepository, @@ -109,11 +120,11 @@ class RadioControllerImpl( private val meshPrefs: MeshPrefs, uiPrefs: UiPrefs, private val databaseManager: DatabaseManager, - private val notificationManager: NotificationManager, + private val serviceNotifications: MeshNotificationManager, private val messageProcessor: Lazy, radioConfigRepository: RadioConfigRepository, - scope: CoroutineScope, - private val onDeviceAddressChanged: (() -> Unit)? = null, + scope: ServiceScope, + private val deviceAddressChangeHook: DeviceAddressChangeHook, ) : RadioController, AdminController by AdminControllerImpl( commandSender = commandSender, @@ -303,7 +314,7 @@ class RadioControllerImpl( } } // Keep callbacks outside the transition mutex so observers cannot deadlock by scheduling another selection. - onDeviceAddressChanged?.invoke() + deviceAddressChangeHook.onDeviceAddressChanged() } override fun requestGattCacheInvalidationOnNextConnect() { @@ -321,14 +332,15 @@ class RadioControllerImpl( messageProcessor.value.clearEarlyPackets() databaseManager.switchActiveDatabase(deviceAddr) nodeManager.clear() - notificationManager.cancelAll() + serviceNotifications.clearNotifications() nodeManager.loadCachedNodeDB() // Commit the persisted selection last. MeshPrefs writes asynchronously, so the transport's synchronous // selected-address snapshot remains the rollback authority for a rapid subsequent selection. meshPrefs.setDeviceAddress(deviceAddr) } - @Suppress("TooGenericExceptionCaught") + // Only called under NonCancellable, so no cancellation of its own reaches attemptRollback's catch. + @Suppress("TooGenericExceptionCaught", "SuspendFunSwallowedCancellation") private suspend fun rollbackDeviceSwitch(previousAddress: String?, originalFailure: Exception) { suspend fun attemptRollback(description: String, block: suspend () -> Unit): Boolean = try { block() @@ -349,7 +361,7 @@ class RadioControllerImpl( attemptRollback("fail-closed connection-identity clear") { nodeManager.clearConnectionIdentity() } attemptRollback("fail-closed node-state clear") { nodeManager.clear() } attemptRollback("fail-closed early-packet clear") { messageProcessor.value.clearEarlyPackets() } - attemptRollback("fail-closed notification clear") { notificationManager.cancelAll() } + attemptRollback("fail-closed notification clear") { serviceNotifications.clearNotifications() } attemptRollback("fail-closed persisted selection") { meshPrefs.setDeviceAddress(null) } attemptRollback("fail-closed transport selection") { check(radioInterfaceService.setDeviceAddress(null)) { "Transport rejected fail-closed deselection" } @@ -362,7 +374,7 @@ class RadioControllerImpl( nodeManager.clearConnectionIdentity() nodeManager.clear() messageProcessor.value.clearEarlyPackets() - notificationManager.cancelAll() + serviceNotifications.clearNotifications() nodeManager.loadCachedNodeDB() } attemptRollback("transport selection") { diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt index baf25cb14f..9d9eac7d72 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt @@ -46,8 +46,16 @@ enum class ServiceStayAliveDecision { * that failed dependency injection or could not enter the foreground — in the latter case ActivityManager has already * armed the pending-start watchdog, and refusing to stop would turn a recoverable refusal into a process kill. */ -fun serviceStayAliveDecision(address: String?, hasActiveRadioOperation: Boolean): ServiceStayAliveDecision = when { - isValidDeviceAddress(address) -> ServiceStayAliveDecision.STAY_FOR_DEVICE +fun serviceStayAliveDecision( + address: String?, + hasActiveRadioOperation: Boolean, + canConnect: (String) -> Boolean, +): ServiceStayAliveDecision = when { + // A saved address this hardware cannot connect to (a serial radio with no USB host) must not hold the service. + address != null && isValidDeviceAddress(address) && canConnect(address) -> + ServiceStayAliveDecision.STAY_FOR_DEVICE + hasActiveRadioOperation -> ServiceStayAliveDecision.STAY_FOR_OPERATION + else -> ServiceStayAliveDecision.STOP } diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt index b2817151df..6f9261c24f 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt @@ -22,6 +22,7 @@ import co.touchlab.kermit.Logger import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -66,6 +67,7 @@ import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.model.DeviceType import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MeshActivity @@ -113,7 +115,11 @@ private data class UsbRecoveryTriggerState( ) private fun selectedSerialPresence(address: String?, keys: Set): SelectedSerialPresence { - val key = address?.takeIf { it.firstOrNull() == InterfaceId.SERIAL.id }?.drop(1)?.takeIf { it.isNotEmpty() } + val key = + DeviceAddress.parse(address) + ?.takeIf { it.interfaceId == InterfaceId.SERIAL } + ?.identity + ?.takeIf { it.isNotEmpty() } return SelectedSerialPresence(key = key, present = key != null && key in keys) } @@ -193,6 +199,11 @@ class SharedRadioInterfaceService( private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr.value) override val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow() + private val selectionLock = SynchronizedObject() + + /** Set by the first [setDeviceAddress]; from then on this process owns the selection, not the prefs mirror. */ + private var selectionPublished = false + // Monotonically increasing generation bumped on every transport start (including same-address reconnect). Exposed // through [sessionGeneration] so the controller layer can clear connection-session identity at each session // boundary instead of only when the selected address changes. @@ -263,24 +274,27 @@ class SharedRadioInterfaceService( } } - override suspend fun runWhileSessionActive(session: RadioSessionContext, block: suspend () -> Unit): Boolean = - sessionOperationMutex.withLock { - runWithSessionLease(session) { - // Bound the handler: it holds sessionOperationMutex (the whole inbound pipeline) and an admitted - // lease (which teardown's drain awaits), so an indefinite suspension here is a total wedge, not a - // slow packet. Cancelling the block releases both. Only OUR timeout is swallowed — ensureActive() - // rethrows if the surrounding scope was cancelled concurrently. - try { - withTimeout(SESSION_HANDLER_TIMEOUT_MILLIS) { block() } - } catch (timeout: TimeoutCancellationException) { - currentCoroutineContext().ensureActive() - Logger.e(timeout) { - "Session handler exceeded ${SESSION_HANDLER_TIMEOUT_MILLIS}ms and was cancelled; " + - "dropping its packet to keep the receive pipeline alive" - } + override suspend fun runWhileSessionActive( + session: RadioSessionContext, + label: String, + block: suspend () -> Unit, + ): Boolean = sessionOperationMutex.withLock { + runWithSessionLease(session) { + // Bound the handler: it holds sessionOperationMutex (the whole inbound pipeline) and an admitted + // lease (which teardown's drain awaits), so an indefinite suspension here is a total wedge, not a + // slow packet. Cancelling the block releases both. Only OUR timeout is swallowed — ensureActive() + // rethrows if the surrounding scope was cancelled concurrently. + try { + withTimeout(SESSION_HANDLER_TIMEOUT_MILLIS) { block() } + } catch (timeout: TimeoutCancellationException) { + currentCoroutineContext().ensureActive() + Logger.e(timeout) { + "Session handler exceeded ${SESSION_HANDLER_TIMEOUT_MILLIS}ms and was cancelled; " + + "dropping its packet to keep the receive pipeline alive (handler=$label)" } } } + } private fun releaseSessionOperation(admittedSession: RadioTransportSession) { val drainWaiter = @@ -566,11 +580,12 @@ class SharedRadioInterfaceService( // starts a transport. Transport start remains driven exclusively by connect() (initial), // setDeviceAddress() (explicit user switch), BLE/network state changes (environment // recovery), and liveness restarts (zombie recovery) — see startTransportLocked() callers. - // _currentDeviceAddressFlow is a MutableStateFlow (atomic .value), so the unconditional - // assignment here is race-free without holding transportMutex; same-address writes are - // idempotent no-ops. + // It stops at the first setDeviceAddress(): prefs commit asynchronously, so a later emission can carry an + // older selection than the one already published. radioPrefs.devAddr - .onEach { addr -> _currentDeviceAddressFlow.value = addr } + .onEach { addr -> + synchronized(selectionLock) { if (!selectionPublished) _currentDeviceAddressFlow.value = addr } + } .catch { Logger.e(it) { "radioPrefs.devAddr address-sync flow crashed" } } .launchIn(processLifecycle.coroutineScope) } @@ -829,7 +844,10 @@ class SharedRadioInterfaceService( Logger.d { "Setting bonded device to ${sanitized?.anonymize}" } radioPrefs.setDevAddr(sanitized) - _currentDeviceAddressFlow.value = sanitized + synchronized(selectionLock) { + selectionPublished = true + _currentDeviceAddressFlow.value = sanitized + } processLifecycle.coroutineScope.launch { transportMutex.withLock { @@ -925,11 +943,12 @@ class SharedRadioInterfaceService( try { withContext(NonCancellable) { newTransport.close() } } catch (closeFailure: Exception) { + if (closeFailure is CancellationException) currentCoroutineContext().ensureActive() publicationFailure.addSuppressed(closeFailure) } throw publicationFailure } - runningTransportId = address.firstOrNull()?.let { InterfaceId.forIdChar(it) } + runningTransportId = DeviceAddress.parse(address)?.interfaceId isStarted = true startHeartbeat() } @@ -997,14 +1016,13 @@ class SharedRadioInterfaceService( private fun startHeartbeat() { heartbeatJob?.cancel() lastDataReceivedMillis = now() - heartbeatJob = - serviceScope.launch { - while (true) { - delay(HEARTBEAT_INTERVAL_MILLIS) - keepAlive() - checkLiveness() - } + heartbeatJob = serviceScope.launch { + while (true) { + delay(HEARTBEAT_INTERVAL_MILLIS) + keepAlive() + checkLiveness() } + } } /** @@ -1129,10 +1147,11 @@ class SharedRadioInterfaceService( private fun sendThroughAdmittedTransport(admission: TransportSendAdmission.Admitted, bytes: ByteArray): Boolean = try { - val sent = - safeCatching { admission.transport.handleSendToRadio(bytes) } - .onFailure { Logger.w(it) { "trySendToRadio: active transport rejected ${bytes.size} bytes" } } - .getOrDefault(false) + val sent = safeCatching { + admission.transport.handleSendToRadio(bytes) + } + .onFailure { Logger.w(it) { "trySendToRadio: active transport rejected ${bytes.size} bytes" } } + .getOrDefault(false) if (sent) { safeCatching { _meshActivity.tryEmit(MeshActivity.Send) } .onFailure { Logger.w(it) { "trySendToRadio: failed to publish mesh activity" } } diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/BootReconnectDecisionTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/BootReconnectDecisionTest.kt index ac3ec89497..f1682cc71e 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/BootReconnectDecisionTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/BootReconnectDecisionTest.kt @@ -53,6 +53,14 @@ class BootReconnectDecisionTest { ) } + @Test + fun `a legacy bang BLE device is not reconnected without the Bluetooth permission`() { + assertEquals( + BootReconnectDecision.BLE_PERMISSION_MISSING, + bootReconnectDecision("!AA:BB:CC:DD:EE:FF", hasBluetoothPermission = false), + ) + } + @Test fun `TCP and USB devices reconnect regardless of the Bluetooth permission`() { // Gating these would break reconnection for users who have deliberately never granted Bluetooth access. diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshLogCleanupTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshLogCleanupTest.kt new file mode 100644 index 0000000000..1acd21163a --- /dev/null +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshLogCleanupTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.repository.MeshLogRetention +import org.meshtastic.core.testing.FakeMeshLogPrefs +import org.meshtastic.core.testing.FakeMeshLogRepository +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +class MeshLogCleanupTest { + private val repository = FakeMeshLogRepository() + private val prefs = + FakeMeshLogPrefs().apply { + setLoggingEnabled(true) + setRetentionDays(7) + } + private val cleanup = MeshLogCleanup(repository, prefs) + + @Test + fun testRunHourlyFirstPrunesShortlyAfterStartThenEveryHour() = runTest { + backgroundScope.launch { cleanup.runHourly() } + + advance(MeshLogCleanup.FIRST_RUN_DELAY - 1.milliseconds) + assertEquals(0, repository.deleteLogsOlderThanCalls) + advance(1.milliseconds) + assertEquals(1, repository.deleteLogsOlderThanCalls) + assertEquals(7, repository.lastDeletedOlderThan) + + advance(MeshLogCleanup.INTERVAL - 1.milliseconds) + assertEquals(1, repository.deleteLogsOlderThanCalls) + advance(1.milliseconds) + assertEquals(2, repository.deleteLogsOlderThanCalls) + } + + @Test + fun testRunHourlyKeepsItsScheduleAfterAFailedPass() = runTest { + repository.beforeDeleteLogsOlderThan = { if (repository.deleteLogsOlderThanCalls == 1) error("disk I/O error") } + backgroundScope.launch { cleanup.runHourly() } + + advance(MeshLogCleanup.FIRST_RUN_DELAY) + assertEquals(1, repository.deleteLogsOlderThanCalls) + assertNull(repository.lastDeletedOlderThan) + + advance(MeshLogCleanup.INTERVAL) + assertEquals(2, repository.deleteLogsOlderThanCalls) + assertEquals(7, repository.lastDeletedOlderThan) + } + + @Test + fun testRunHourlyStopsWhenItsScopeIsCancelled() = runTest { + val schedule = backgroundScope.launch { cleanup.runHourly() } + advance(MeshLogCleanup.FIRST_RUN_DELAY) + assertEquals(1, repository.deleteLogsOlderThanCalls) + + schedule.cancel() + advance(MeshLogCleanup.INTERVAL * 3) + + assertEquals(1, repository.deleteLogsOlderThanCalls) + } + + @Test + fun testRunOnceSkipsWhenLoggingIsDisabled() = runTest { + prefs.setLoggingEnabled(false) + + cleanup.runOnce() + + assertEquals(0, repository.deleteLogsOlderThanCalls) + } + + @Test + fun testRunOnceSkipsWhenRetentionKeepsLogsForever() = runTest { + prefs.setRetentionDays(MeshLogRetention.KEEP_FOREVER) + + cleanup.runOnce() + + assertEquals(0, repository.deleteLogsOlderThanCalls) + } + + @Test + fun testRunOnceKeepsTheOneHourSentinel() = runTest { + prefs.setRetentionDays(MeshLogRetention.ONE_HOUR) + + cleanup.runOnce() + + assertEquals(MeshLogRetention.ONE_HOUR, repository.lastDeletedOlderThan) + } + + private fun TestScope.advance(by: Duration) { + advanceTimeBy(by) + runCurrent() + } +} diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt index 8e2132b9d0..f01d56eab7 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt @@ -219,16 +219,8 @@ class MeshServiceOrchestratorTest { val takEnabledFlow = MutableStateFlow(false) val takRunningFlow = MutableStateFlow(false) val lifecycleEvents = mutableListOf() - every { takServerManager.start(any()) } calls - { - lifecycleEvents += "start" - Unit - } - every { takServerManager.stop() } calls - { - lifecycleEvents += "stop" - Unit - } + every { takServerManager.start(any()) } calls { lifecycleEvents += "start" } + every { takServerManager.stop() } calls { lifecycleEvents += "stop" } val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow) orchestrator.start() @@ -248,16 +240,8 @@ class MeshServiceOrchestratorTest { val takEnabledFlow = MutableStateFlow(true) val takRunningFlow = MutableStateFlow(false) val lifecycleEvents = mutableListOf() - every { takServerManager.start(any()) } calls - { - lifecycleEvents += "start" - Unit - } - every { takServerManager.stop() } calls - { - lifecycleEvents += "stop" - Unit - } + every { takServerManager.start(any()) } calls { lifecycleEvents += "start" } + every { takServerManager.stop() } calls { lifecycleEvents += "stop" } val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow) orchestrator.start() diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/PhonePositionTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/PhonePositionTest.kt new file mode 100644 index 0000000000..65eafbcf7a --- /dev/null +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/PhonePositionTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import org.meshtastic.proto.Position as ProtoPosition + +class PhonePositionTest { + + private fun fix( + mslAltitudeMeters: Double? = null, + haeAltitudeMeters: Double? = null, + speedMetersPerSecond: Float? = null, + bearingDegrees: Float? = null, + ) = phonePosition( + latitude = 1.5, + longitude = -2.25, + timeMillis = 1_700_000_000_500, + mslAltitudeMeters = mslAltitudeMeters, + haeAltitudeMeters = haeAltitudeMeters, + speedMetersPerSecond = speedMetersPerSecond, + bearingDegrees = bearingDegrees, + ) + + @Test + fun `readings the fix lacks stay unset`() { + val position = fix() + + assertNull(position.altitude) + assertNull(position.altitude_hae) + assertNull(position.ground_speed) + assertNull(position.ground_track) + } + + @Test + fun `zero readings are sent as zero`() { + val position = + fix(mslAltitudeMeters = 0.0, haeAltitudeMeters = 0.0, speedMetersPerSecond = 0f, bearingDegrees = 0f) + + assertEquals(0, position.altitude) + assertEquals(0, position.altitude_hae) + assertEquals(0, position.ground_speed) + assertEquals(0, position.ground_track) + } + + @Test + fun `speed goes out in km per hour`() { + assertEquals(36, fix(speedMetersPerSecond = 10f).ground_speed) + assertEquals(5, fix(speedMetersPerSecond = 1.4f).ground_speed) + } + + @Test + fun `track goes out in hundred-thousandths of a degree`() { + assertEquals(9_000_000, fix(bearingDegrees = 90f).ground_track) + assertEquals(35_950_000, fix(bearingDegrees = 359.5f).ground_track) + } + + @Test + fun `each altitude keeps its own datum`() { + val position = fix(mslAltitudeMeters = 12.9, haeAltitudeMeters = -20.4) + + assertEquals(12, position.altitude) + assertEquals(-20, position.altitude_hae) + } + + @Test + fun `coordinates and time use the wire scale`() { + val position = fix() + + assertEquals(15_000_000, position.latitude_i) + assertEquals(-22_500_000, position.longitude_i) + assertEquals(1_700_000_000, position.time) + assertEquals(ProtoPosition.LocSource.LOC_EXTERNAL, position.location_source) + } +} diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt index f70f191196..521cf6e195 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.common.di.asServiceScope import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus @@ -58,10 +59,10 @@ import org.meshtastic.core.repository.LocalNodeUnavailableException import org.meshtastic.core.repository.MeshDataHandler import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.PacketQueueRejectedException import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.PlatformAnalytics @@ -104,7 +105,7 @@ class RadioControllerImplTest { private val meshPrefs: MeshPrefs = mock(MockMode.autofill) private val uiPrefs: UiPrefs = mock(MockMode.autofill) private val databaseManager: DatabaseManager = mock(MockMode.autofill) - private val notificationManager: NotificationManager = mock(MockMode.autofill) + private val serviceNotifications: MeshNotificationManager = mock(MockMode.autofill) private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill) private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) @@ -126,12 +127,12 @@ class RadioControllerImplTest { activeSession ?: MutableStateFlow(deviceAddress.value?.let { RadioSessionContext(sessionGeneration.value, it) }) every { radioInterfaceService.activeSession } returns resolvedActiveSession - everySuspend { radioInterfaceService.runWhileSessionActive(any(), any()) } calls + everySuspend { radioInterfaceService.runWhileSessionActive(any(), any(), any()) } calls { val session = it.args[0] as RadioSessionContext @Suppress("UNCHECKED_CAST") - val block = it.args[1] as (suspend () -> Unit) + val block = it.args[2] as (suspend () -> Unit) if (resolvedActiveSession.value == session) { block() true @@ -173,11 +174,11 @@ class RadioControllerImplTest { meshPrefs = meshPrefs, uiPrefs = uiPrefs, databaseManager = databaseManager, - notificationManager = notificationManager, + serviceNotifications = serviceNotifications, messageProcessor = lazy { messageProcessor }, radioConfigRepository = radioConfigRepository, - scope = scope, - onDeviceAddressChanged = onDeviceAddressChanged, + scope = scope.asServiceScope(), + deviceAddressChangeHook = { onDeviceAddressChanged?.invoke() }, ) } diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt index 7b7a83171b..0eb073c83e 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt @@ -33,15 +33,16 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.common.di.asServiceScope import org.meshtastic.core.repository.AdminEditScope import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshDataHandler import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioConfigRepository @@ -71,7 +72,7 @@ class RadioControllerRestoreTest { private val dataHandler: MeshDataHandler = mock(MockMode.autofill) private val analytics: PlatformAnalytics = mock(MockMode.autofill) private val uiPrefs: UiPrefs = mock(MockMode.autofill) - private val notificationManager: NotificationManager = mock(MockMode.autofill) + private val serviceNotifications: MeshNotificationManager = mock(MockMode.autofill) private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill) private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) @@ -99,10 +100,11 @@ class RadioControllerRestoreTest { meshPrefs = meshPrefs, uiPrefs = uiPrefs, databaseManager = databaseManager, - notificationManager = notificationManager, + serviceNotifications = serviceNotifications, messageProcessor = lazy { messageProcessor }, radioConfigRepository = radioConfigRepository, - scope = scope, + scope = scope.asServiceScope(), + deviceAddressChangeHook = {}, ) } } diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt index 92ac52db20..c6fdc13c14 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt @@ -21,11 +21,19 @@ import kotlin.test.assertEquals class ServiceStayAliveDecisionTest { + private val anyAddressConnects: (String) -> Boolean = { true } + + private val noAddressConnects: (String) -> Boolean = { false } + @Test fun selectedDeviceKeepsServiceAlive() { assertEquals( ServiceStayAliveDecision.STAY_FOR_DEVICE, - serviceStayAliveDecision(address = "x11:22:33:44:55:66", hasActiveRadioOperation = false), + serviceStayAliveDecision( + address = "x11:22:33:44:55:66", + hasActiveRadioOperation = false, + canConnect = anyAddressConnects, + ), ) } @@ -33,7 +41,11 @@ class ServiceStayAliveDecisionTest { fun selectedDeviceWinsOverOperation() { assertEquals( ServiceStayAliveDecision.STAY_FOR_DEVICE, - serviceStayAliveDecision(address = "t10.0.2.2:4403", hasActiveRadioOperation = true), + serviceStayAliveDecision( + address = "t10.0.2.2:4403", + hasActiveRadioOperation = true, + canConnect = anyAddressConnects, + ), ) } @@ -42,7 +54,7 @@ class ServiceStayAliveDecisionTest { // The exact firmware-update case: the flow sets the address to the "none" sentinel to free the transport. assertEquals( ServiceStayAliveDecision.STAY_FOR_OPERATION, - serviceStayAliveDecision(address = "n", hasActiveRadioOperation = true), + serviceStayAliveDecision(address = "n", hasActiveRadioOperation = true, canConnect = anyAddressConnects), ) } @@ -50,7 +62,7 @@ class ServiceStayAliveDecisionTest { fun noAddressDuringAnOperationStaysAlive() { assertEquals( ServiceStayAliveDecision.STAY_FOR_OPERATION, - serviceStayAliveDecision(address = null, hasActiveRadioOperation = true), + serviceStayAliveDecision(address = null, hasActiveRadioOperation = true, canConnect = anyAddressConnects), ) } @@ -58,7 +70,7 @@ class ServiceStayAliveDecisionTest { fun deselectedWithNothingRunningStops() { assertEquals( ServiceStayAliveDecision.STOP, - serviceStayAliveDecision(address = "n", hasActiveRadioOperation = false), + serviceStayAliveDecision(address = "n", hasActiveRadioOperation = false, canConnect = anyAddressConnects), ) } @@ -66,11 +78,35 @@ class ServiceStayAliveDecisionTest { fun blankAddressWithNothingRunningStops() { assertEquals( ServiceStayAliveDecision.STOP, - serviceStayAliveDecision(address = "", hasActiveRadioOperation = false), + serviceStayAliveDecision(address = "", hasActiveRadioOperation = false, canConnect = anyAddressConnects), ) assertEquals( ServiceStayAliveDecision.STOP, - serviceStayAliveDecision(address = null, hasActiveRadioOperation = false), + serviceStayAliveDecision(address = null, hasActiveRadioOperation = false, canConnect = anyAddressConnects), + ) + } + + @Test + fun savedAddressThatCannotConnectStops() { + assertEquals( + ServiceStayAliveDecision.STOP, + serviceStayAliveDecision( + address = "s1027:29987:0", + hasActiveRadioOperation = false, + canConnect = noAddressConnects, + ), + ) + } + + @Test + fun savedAddressThatCannotConnectStillStaysForAnOperation() { + assertEquals( + ServiceStayAliveDecision.STAY_FOR_OPERATION, + serviceStayAliveDecision( + address = "s1027:29987:0", + hasActiveRadioOperation = true, + canConnect = noAddressConnects, + ), ) } } diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt index ebd2d67a99..fb149860d3 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner +import co.touchlab.kermit.Severity import dev.mokkery.MockMode import dev.mokkery.answering.calls import dev.mokkery.answering.returns @@ -42,17 +43,22 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.network.radio.BaseRadioTransportFactory import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.network.repository.SerialDevicePresence import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.RadioTransport import org.meshtastic.core.repository.RadioTransportFactory import org.meshtastic.core.repository.TransportDisconnectReason +import org.meshtastic.core.testing.CapturingLogWriter import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeRadioPrefs import org.meshtastic.core.testing.FakeRadioTransport @@ -284,6 +290,7 @@ class SharedRadioInterfaceServiceLivenessTest { transportProvider: () -> RadioTransport = { FakeRadioTransport().also { createdTransports.add(it) } }, networkAvailability: MutableStateFlow = MutableStateFlow(true), startConnected: Boolean = true, + radioPrefs: RadioPrefs = this.radioPrefs, ): SharedRadioInterfaceService { every { networkRepository.networkAvailable } returns networkAvailability every { networkRepository.resolvedList } returns MutableSharedFlow() @@ -387,6 +394,121 @@ class SharedRadioInterfaceServiceLivenessTest { } } + /** + * The path `MeshServiceOrchestrator.coldStartConnect` takes with a persisted `x` address restored from a backup. + */ + @Test + fun `cold start with a saved BLE address arms no transport on hardware without Bluetooth`() = + runTest(testDispatcher) { + bluetoothRepository.isSupported = false + every { networkRepository.networkAvailable } returns MutableStateFlow(true) + every { networkRepository.resolvedList } returns MutableSharedFlow() + every { analytics.isPlatformServicesAvailable } returns false + val requestedTransports = mutableListOf() + val realFactory = + object : + BaseRadioTransportFactory( + scanner = mock(MockMode.autofill), + bluetoothRepository = bluetoothRepository, + connectionFactory = mock(MockMode.autofill), + dispatchers = dispatchers, + ) { + override val supportedDeviceTypes: List = listOf(DeviceType.TCP) + + override val mockTransportEnabled: StateFlow = MutableStateFlow(false) + + override val isReplayTransportAvailable: Boolean = false + + override fun createTransport(address: String, service: RadioInterfaceService): RadioTransport { + requestedTransports += address + return super.createTransport(address, service) + } + + override fun createPlatformTransport(address: String, service: RadioInterfaceService) = + FakeRadioTransport() + } + val savedAddress = "xAA:BB:CC:DD:EE:FF" + radioPrefs.setDevAddr(savedAddress) + bluetoothRepository.setBluetoothEnabled(false) + val service = + SharedRadioInterfaceService( + dispatchers = dispatchers, + bluetoothRepository = bluetoothRepository, + networkRepository = networkRepository, + serialDevicePresence = serialDevicePresence, + processLifecycle = processLifecycleOwner.lifecycle, + radioPrefs = radioPrefs, + transportFactory = realFactory, + analytics = analytics, + radioOperationLock = radioOperationLock, + ) + try { + service.connect() + // A Bluetooth-state recovery must not arm it either. + bluetoothRepository.setBluetoothEnabled(true) + + assertTrue(requestedTransports.isEmpty(), "no transport may be built for an unusable BLE address") + assertNull(service.activeSession.value) + assertEquals(ConnectionState.Disconnected, service.connectionState.value) + assertEquals(savedAddress, radioPrefs.devAddr.value, "the saved address is kept, not cleared") + } finally { + service.disconnect() + } + } + + /** Persists like DataStore: a write lands only when the test commits it, in order. */ + private class DeferredRadioPrefs(saved: String?) : RadioPrefs { + override val devAddr = MutableStateFlow(saved) + override val devName = MutableStateFlow(null) + private val pending = ArrayDeque() + + override fun setDevAddr(address: String?) { + pending.addLast(address) + } + + override fun setDevName(name: String?) { + devName.value = name + } + + fun commitThrough(address: String) { + do { + val next = pending.removeFirst() + devAddr.value = next + } while (next != address) + } + } + + @Test + fun `a late commit of an older selection does not rewind the selected address`() = runTest(testDispatcher) { + val prefs = DeferredRadioPrefs(saved = "xAA:AA:AA:AA:AA:AA") + val service = createConnectedService("xAA:AA:AA:AA:AA:AA", startConnected = false, radioPrefs = prefs) + try { + service.setDeviceAddress("xBB:BB:BB:BB:BB:BB") + service.setDeviceAddress("xCC:CC:CC:CC:CC:CC") + + prefs.commitThrough("xBB:BB:BB:BB:BB:BB") + testDispatcher.scheduler.runCurrent() + + assertEquals("xCC:CC:CC:CC:CC:CC", service.currentDeviceAddressFlow.value) + } finally { + service.disconnect() + } + } + + @Test + fun `the saved address still reaches the flow when it loads after construction`() = runTest(testDispatcher) { + val prefs = DeferredRadioPrefs(saved = null) + val service = createConnectedService("xAA:AA:AA:AA:AA:AA", startConnected = false, radioPrefs = prefs) + try { + prefs.devAddr.value = "xAA:AA:AA:AA:AA:AA" + testDispatcher.scheduler.runCurrent() + + assertEquals("xAA:AA:AA:AA:AA:AA", service.currentDeviceAddressFlow.value) + } finally { + service.disconnect() + } + } + @Test fun `setDeviceAddress contains factory failure and same-address repair can retry`() = runTest(testDispatcher) { bluetoothRepository.setBluetoothEnabled(false) @@ -435,13 +557,13 @@ class SharedRadioInterfaceServiceLivenessTest { val independentStarted = CompletableDeferred() val first = launch { - service.runWhileSessionActive(session) { + service.runWhileSessionActive(session, "first") { firstStarted.complete(Unit) releaseFirst.await() } } firstStarted.await() - val second = launch { service.runWhileSessionActive(session) { secondStarted.complete(Unit) } } + val second = launch { service.runWhileSessionActive(session, "second") { secondStarted.complete(Unit) } } val independent = launch { service.runWithSessionLease(session) { independentStarted.complete(Unit) } } try { testDispatcher.scheduler.runCurrent() @@ -499,7 +621,7 @@ class SharedRadioInterfaceServiceLivenessTest { ) assertFalse(service.isSessionActive(session), "teardown must reject new work immediately") assertFalse( - service.runWhileSessionActive(session) { error("late operation must not run") }, + service.runWhileSessionActive(session, "late") { error("late operation must not run") }, "work queued after admission closes must be rejected", ) disconnectJob.cancel() @@ -582,6 +704,28 @@ class SharedRadioInterfaceServiceLivenessTest { } } + @Test + fun `BLE liveness timeout restarts a transport saved with the legacy bang prefix`() = runTest(testDispatcher) { + clock = 0L + val service = createConnectedService("!AA:BB:CC:DD:EE:FF") + try { + clock = 65_000L + service.checkLiveness() + testDispatcher.scheduler.runCurrent() + advanceTimeBy(1_000L) + + assertEquals( + 2, + createdTransports.size, + "A silent legacy BLE link should be restarted like any BLE link", + ) + assertTrue(createdTransports.first().closeCalled, "Old transport must be closed") + } finally { + service.disconnect() + advanceTimeBy(1_000L) + } + } + @Test fun `BLE liveness restart contains factory failure and a later connect can retry`() = runTest(testDispatcher) { clock = 0L @@ -1024,8 +1168,9 @@ class SharedRadioInterfaceServiceLivenessTest { val neverReleased = CompletableDeferred() var wedgeRanToCompletion = false + val logs = CapturingLogWriter.install() val wedged = launch { - service.runWhileSessionActive(session) { + service.runWhileSessionActive(session, "FromRadio packet TEXT_MESSAGE_APP") { wedgeStarted.complete(Unit) neverReleased.await() // simulates a handler stuck on an unbounded suspension wedgeRanToCompletion = true @@ -1033,7 +1178,7 @@ class SharedRadioInterfaceServiceLivenessTest { } wedgeStarted.await() val nextStarted = CompletableDeferred() - val next = launch { service.runWhileSessionActive(session) { nextStarted.complete(Unit) } } + val next = launch { service.runWhileSessionActive(session, "next") { nextStarted.complete(Unit) } } try { testDispatcher.scheduler.runCurrent() assertFalse(nextStarted.isCompleted, "ordered work is serialized behind the wedged handler") @@ -1045,7 +1190,14 @@ class SharedRadioInterfaceServiceLivenessTest { assertFalse(wedgeRanToCompletion, "the wedged handler must have been cancelled, not completed") assertTrue(nextStarted.isCompleted, "the handler timeout must release the pipeline for queued work") + val timeoutLines = logs.messages(Severity.Error).filter { "Session handler exceeded" in it } + assertEquals(1, timeoutLines.size, "one timeout line: $timeoutLines") + assertTrue( + "(handler=FromRadio packet TEXT_MESSAGE_APP)" in timeoutLines.single(), + "the timeout line must name the stuck handler: $timeoutLines", + ) } finally { + CapturingLogWriter.uninstall() neverReleased.complete(Unit) wedged.cancel() next.cancel() @@ -1068,7 +1220,7 @@ class SharedRadioInterfaceServiceLivenessTest { val neverReleased = CompletableDeferred() val wedged = launch { - service.runWhileSessionActive(session) { + service.runWhileSessionActive(session, "wedged") { wedgeStarted.complete(Unit) neverReleased.await() } diff --git a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/DesktopKeystoreCipher.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/DesktopKeystoreCipher.kt new file mode 100644 index 0000000000..b6797379f5 --- /dev/null +++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/DesktopKeystoreCipher.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +/** + * AES-256-GCM encryption for the desktop secure stores, keyed by a master key held in a PKCS12 keystore in [dir]. + * + * The keystore password is fixed because the threat model mirrors Android's `EncryptedSharedPreferences`: file-system + * permission is the primary access control; the encryption layer protects data at rest against casual file browsing or + * backup leakage, not against a compromised user account. + */ +internal class DesktopKeystoreCipher( + private val dir: File, + private val keyAlias: String, + private val keystorePassword: CharArray, +) { + /** + * Loads the master key, creating the keystore and key on first use. + * + * Throws when the keystore exists but holds no key under [keyAlias]: generating a replacement would overwrite the + * keystore and orphan every file encrypted under the old key. + */ + fun loadOrCreateMasterKey(): SecretKey { + val ksFile = File(dir, KEYSTORE_FILE) + val ks = KeyStore.getInstance(KEYSTORE_TYPE) + val protection = KeyStore.PasswordProtection(keystorePassword) + if (ksFile.exists()) { + FileInputStream(ksFile).use { ks.load(it, keystorePassword) } + val entry = ks.getEntry(keyAlias, protection) + check(entry is KeyStore.SecretKeyEntry) { "Keystore exists but master key $keyAlias is missing/invalid" } + return entry.secretKey + } + val keyGen = KeyGenerator.getInstance(AES_ALGORITHM) + keyGen.init(AES_KEY_BITS) + val secretKey = keyGen.generateKey() + ks.load(null, keystorePassword) + ks.setEntry(keyAlias, KeyStore.SecretKeyEntry(secretKey), protection) + FileOutputStream(ksFile).use { ks.store(it, keystorePassword) } + return secretKey + } + + /** Encrypts [plaintext] as `[1 byte IV length][IV][ciphertext]`. */ + fun encrypt(key: SecretKey, plaintext: ByteArray): ByteArray { + val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) + cipher.init(Cipher.ENCRYPT_MODE, key) + val iv = cipher.iv + val ciphertext = cipher.doFinal(plaintext) + return byteArrayOf(iv.size.toByte()) + iv + ciphertext + } + + fun decrypt(key: SecretKey, data: ByteArray): ByteArray { + val ivLength = data[0].toInt() and BYTE_MASK + val iv = data.copyOfRange(1, 1 + ivLength) + val ciphertext = data.copyOfRange(1 + ivLength, data.size) + val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) + cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_BITS, iv)) + return cipher.doFinal(ciphertext) + } + + private companion object { + private const val KEYSTORE_FILE = "keystore.p12" + private const val KEYSTORE_TYPE = "PKCS12" + private const val AES_ALGORITHM = "AES" + private const val AES_GCM_TRANSFORM = "AES/GCM/NoPadding" + private const val AES_KEY_BITS = 256 + private const val GCM_TAG_BITS = 128 + private const val BYTE_MASK = 0xFF + } +} diff --git a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/JvmFileService.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/JvmFileService.kt index 5b3d6df0d6..4d277975a5 100644 --- a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/JvmFileService.kt +++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/JvmFileService.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.service import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withContext import okio.BufferedSink import okio.BufferedSource @@ -28,17 +29,19 @@ import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.FileService import java.io.File +import java.net.URI @Single class JvmFileService(private val dispatchers: CoroutineDispatchers) : FileService { override suspend fun write(uri: CommonUri, block: suspend (BufferedSink) -> Unit): Boolean = withContext(dispatchers.io) { try { - // Treat URI string as a local file path - val file = File(uri.toString()) + val file = uri.toFile() file.parentFile?.mkdirs() file.sink().buffer().use { sink -> block(sink) } true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to write to URI: $uri" } false @@ -48,12 +51,21 @@ class JvmFileService(private val dispatchers: CoroutineDispatchers) : FileServic override suspend fun read(uri: CommonUri, block: suspend (BufferedSource) -> Unit): Boolean = withContext(dispatchers.io) { try { - val file = File(uri.toString()) + val file = uri.toFile() file.source().buffer().use { source -> block(source) } true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to read from URI: $uri" } false } } + + /** The desktop file pickers hand back `file:` URIs; anything else is taken as a plain path. */ + private fun CommonUri.toFile(): File { + val text = toString() + val parsed = runCatching { URI(text) }.getOrNull() + return if (parsed?.scheme == "file") File(parsed) else File(text) + } } diff --git a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt index 868f72a75c..d93d076f6f 100644 --- a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt +++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt @@ -22,34 +22,25 @@ import org.meshtastic.core.database.desktopDataDir import org.meshtastic.core.repository.LockdownPassphraseStore import org.meshtastic.core.repository.StoredPassphrase import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec /** * File-backed encrypted passphrase store for JVM/Desktop. * - * Uses a PKCS12 KeyStore to hold an AES-256 master key and AES-256-GCM to encrypt each passphrase entry. Entries are - * stored as individual `.enc` files under `$MESHTASTIC_DATA_DIR/lockdown/` (default: `~/.meshtastic/lockdown/`), keyed - * by a sanitized device address. - * - * The keystore password is fixed because the threat model mirrors Android's `EncryptedSharedPreferences`: file-system - * permission is the primary access control; the encryption layer protects data at rest against casual file browsing or - * backup leakage, not against a compromised user account. + * Each passphrase entry is an AES-256-GCM `.enc` file under `$MESHTASTIC_DATA_DIR/lockdown/` (default: + * `~/.meshtastic/lockdown/`), keyed by a sanitized device address; see [DesktopKeystoreCipher] for the key handling. */ @Single(binds = [LockdownPassphraseStore::class]) @Suppress("TooGenericExceptionCaught") -class LockdownPassphraseStoreImpl : LockdownPassphraseStore { +class LockdownPassphraseStoreImpl(dataDir: File = File(desktopDataDir())) : LockdownPassphraseStore { - private val lockdownDir: File by lazy { File(desktopDataDir(), LOCKDOWN_DIR).also { it.mkdirs() } } + private val lockdownDir: File by lazy { File(dataDir, LOCKDOWN_DIR).also { it.mkdirs() } } + + private val cipher by lazy { DesktopKeystoreCipher(lockdownDir, KEY_ALIAS, KEYSTORE_PASSWORD) } private val masterKey: SecretKey? by lazy { try { - loadOrCreateMasterKey() + cipher.loadOrCreateMasterKey() } catch (e: Exception) { Logger.e(e) { "Lockdown: Failed to initialize desktop keystore" } null @@ -62,8 +53,7 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore { val file = entryFile(deviceAddress) if (!file.exists()) return null return try { - val encrypted = file.readBytes() - val plaintext = decrypt(key, encrypted) + val plaintext = cipher.decrypt(key, file.readBytes()) deserialize(plaintext) } catch (e: Exception) { Logger.e(e) { "Lockdown: Failed to read passphrase for device" } @@ -80,8 +70,7 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore { ) { val key = masterKey ?: error("Lockdown: Cannot save passphrase - keystore unavailable") val plaintext = serialize(passphrase, boots, hours, maxSessionSeconds) - val encrypted = encrypt(key, plaintext) - entryFile(deviceAddress).writeBytes(encrypted) + entryFile(deviceAddress).writeBytes(cipher.encrypt(key, plaintext)) } override fun clearPassphrase(deviceAddress: String) { @@ -96,28 +85,6 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore { return File(lockdownDir, "$sanitized.enc") } - // region Encryption - - private fun encrypt(key: SecretKey, plaintext: ByteArray): ByteArray { - val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) - cipher.init(Cipher.ENCRYPT_MODE, key) - val iv = cipher.iv - val ciphertext = cipher.doFinal(plaintext) - // Format: [1 byte IV length][IV][ciphertext] - return byteArrayOf(iv.size.toByte()) + iv + ciphertext - } - - private fun decrypt(key: SecretKey, data: ByteArray): ByteArray { - val ivLength = data[0].toInt() and BYTE_MASK - val iv = data.copyOfRange(1, 1 + ivLength) - val ciphertext = data.copyOfRange(1 + ivLength, data.size) - val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) - cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_BITS, iv)) - return cipher.doFinal(ciphertext) - } - - // endregion - // region Serialization (simple line-based to avoid adding kotlinx-serialization dependency) // Format v2: "boots\nhours\nmaxSessionSeconds\npassphrase" (4 lines). @@ -160,42 +127,12 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore { // endregion - // region KeyStore - - private fun loadOrCreateMasterKey(): SecretKey { - val ksFile = File(lockdownDir, KEYSTORE_FILE) - val ks = KeyStore.getInstance(KEYSTORE_TYPE) - val protection = KeyStore.PasswordProtection(KEYSTORE_PASSWORD) - if (ksFile.exists()) { - FileInputStream(ksFile).use { ks.load(it, KEYSTORE_PASSWORD) } - val entry = ks.getEntry(KEY_ALIAS, protection) - if (entry is KeyStore.SecretKeyEntry) return entry.secretKey - } - // Generate new master key - val keyGen = KeyGenerator.getInstance(AES_ALGORITHM) - keyGen.init(AES_KEY_BITS) - val secretKey = keyGen.generateKey() - ks.load(null, KEYSTORE_PASSWORD) - ks.setEntry(KEY_ALIAS, KeyStore.SecretKeyEntry(secretKey), protection) - FileOutputStream(ksFile).use { ks.store(it, KEYSTORE_PASSWORD) } - return secretKey - } - - // endregion - private companion object { private const val LOCKDOWN_DIR = "lockdown" - private const val KEYSTORE_FILE = "keystore.p12" - private const val KEYSTORE_TYPE = "PKCS12" private const val KEY_ALIAS = "lockdown_master" // Intentional: this mirrors the documented desktop threat model for at-rest protection only. private val KEYSTORE_PASSWORD = "meshtastic-lockdown".toCharArray() - private const val AES_ALGORITHM = "AES" - private const val AES_GCM_TRANSFORM = "AES/GCM/NoPadding" - private const val AES_KEY_BITS = 256 - private const val GCM_TAG_BITS = 128 - private const val BYTE_MASK = 0xFF private const val SERIALIZED_LINE_COUNT_V1 = 3 private const val SERIALIZED_LINE_COUNT_V2 = 4 } diff --git a/core/testing/src/jvmMain/kotlin/org/meshtastic/core/testing/Location.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/NoopDeviceAddressChangeHook.kt similarity index 71% rename from core/testing/src/jvmMain/kotlin/org/meshtastic/core/testing/Location.kt rename to core/service/src/jvmMain/kotlin/org/meshtastic/core/service/NoopDeviceAddressChangeHook.kt index 71a266fb6c..b8b6d81e78 100644 --- a/core/testing/src/jvmMain/kotlin/org/meshtastic/core/testing/Location.kt +++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/NoopDeviceAddressChangeHook.kt @@ -14,9 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.testing +package org.meshtastic.core.service -import org.meshtastic.core.repository.Location +import org.koin.core.annotation.Single -/** Creates a placeholder JVM [Location] for testing. */ -actual fun createLocation(latitude: Double, longitude: Double, altitude: Double): Location = Location() +/** Desktop runs the service in-process, so a new address needs nothing started. */ +@Single +class NoopDeviceAddressChangeHook : DeviceAddressChangeHook { + override fun onDeviceAddressChanged() = Unit +} diff --git a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt index 891338d5d5..2838ce9f66 100644 --- a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt +++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt @@ -22,28 +22,24 @@ import org.meshtastic.core.database.desktopDataDir import org.meshtastic.core.repository.SecurityKeyBackupStore import org.meshtastic.core.repository.StoredSecurityKeys import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec /** - * File-backed encrypted key-backup store for JVM/Desktop, mirroring [LockdownPassphraseStoreImpl]'s desktop - * counterpart. Uses a PKCS12 KeyStore to hold an AES-256 master key and AES-256-GCM to encrypt each node's key backup, - * stored as individual `.enc` files under `$MESHTASTIC_DATA_DIR/security_keys/`. + * File-backed encrypted key-backup store for JVM/Desktop, mirroring [LockdownPassphraseStoreImpl]. Each node's key + * backup is an AES-256-GCM `.enc` file under `$MESHTASTIC_DATA_DIR/security_keys/`; see [DesktopKeystoreCipher] for the + * key handling. */ @Single(binds = [SecurityKeyBackupStore::class]) @Suppress("TooGenericExceptionCaught") -class SecurityKeyBackupStoreImpl : SecurityKeyBackupStore { +class SecurityKeyBackupStoreImpl(dataDir: File = File(desktopDataDir())) : SecurityKeyBackupStore { - private val storeDir: File by lazy { File(desktopDataDir(), STORE_DIR).also { it.mkdirs() } } + private val storeDir: File by lazy { File(dataDir, STORE_DIR).also { it.mkdirs() } } + + private val cipher by lazy { DesktopKeystoreCipher(storeDir, KEY_ALIAS, KEYSTORE_PASSWORD) } private val masterKey: SecretKey? by lazy { try { - loadOrCreateMasterKey() + cipher.loadOrCreateMasterKey() } catch (e: Exception) { Logger.e(e) { "SecurityKeyBackup: Failed to initialize desktop keystore" } null @@ -56,7 +52,7 @@ class SecurityKeyBackupStoreImpl : SecurityKeyBackupStore { val file = entryFile(nodeNum) if (!file.exists()) return null return try { - val plaintext = decrypt(key, file.readBytes()) + val plaintext = cipher.decrypt(key, file.readBytes()) deserialize(plaintext) } catch (e: Exception) { Logger.e(e) { "SecurityKeyBackup: Failed to read key backup for node" } @@ -67,7 +63,7 @@ class SecurityKeyBackupStoreImpl : SecurityKeyBackupStore { override fun save(nodeNum: Int, publicKeyBase64: String, privateKeyBase64: String, timestamp: Long) { val key = masterKey ?: error("SecurityKeyBackup: Cannot save keys - keystore unavailable") val plaintext = "$timestamp\n$publicKeyBase64\n$privateKeyBase64".encodeToByteArray() - entryFile(nodeNum).writeBytes(encrypt(key, plaintext)) + entryFile(nodeNum).writeBytes(cipher.encrypt(key, plaintext)) } override fun delete(nodeNum: Int) { @@ -90,65 +86,12 @@ class SecurityKeyBackupStoreImpl : SecurityKeyBackupStore { return StoredSecurityKeys(publicKeyBase64 = parts[1], privateKeyBase64 = parts[2], timestamp = timestamp) } - // region Encryption - - private fun encrypt(key: SecretKey, plaintext: ByteArray): ByteArray { - val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) - cipher.init(Cipher.ENCRYPT_MODE, key) - val iv = cipher.iv - val ciphertext = cipher.doFinal(plaintext) - // Format: [1 byte IV length][IV][ciphertext] - return byteArrayOf(iv.size.toByte()) + iv + ciphertext - } - - private fun decrypt(key: SecretKey, data: ByteArray): ByteArray { - val ivLength = data[0].toInt() and BYTE_MASK - val iv = data.copyOfRange(1, 1 + ivLength) - val ciphertext = data.copyOfRange(1 + ivLength, data.size) - val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) - cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_BITS, iv)) - return cipher.doFinal(ciphertext) - } - - // endregion - - // region KeyStore - - private fun loadOrCreateMasterKey(): SecretKey { - val ksFile = File(storeDir, KEYSTORE_FILE) - val ks = KeyStore.getInstance(KEYSTORE_TYPE) - val protection = KeyStore.PasswordProtection(KEYSTORE_PASSWORD) - if (ksFile.exists()) { - FileInputStream(ksFile).use { ks.load(it, KEYSTORE_PASSWORD) } - val entry = ks.getEntry(KEY_ALIAS, protection) - // Fail loudly rather than regenerate: overwriting the master key would orphan every existing .enc backup. - check(entry is KeyStore.SecretKeyEntry) { "Keystore exists but master key $KEY_ALIAS is missing/invalid" } - return entry.secretKey - } - val keyGen = KeyGenerator.getInstance(AES_ALGORITHM) - keyGen.init(AES_KEY_BITS) - val secretKey = keyGen.generateKey() - ks.load(null, KEYSTORE_PASSWORD) - ks.setEntry(KEY_ALIAS, KeyStore.SecretKeyEntry(secretKey), protection) - FileOutputStream(ksFile).use { ks.store(it, KEYSTORE_PASSWORD) } - return secretKey - } - - // endregion - private companion object { private const val STORE_DIR = "security_keys" - private const val KEYSTORE_FILE = "keystore.p12" - private const val KEYSTORE_TYPE = "PKCS12" private const val KEY_ALIAS = "security_key_backup_master" // Intentional: mirrors LockdownPassphraseStoreImpl's documented desktop threat model. private val KEYSTORE_PASSWORD = "meshtastic-security-keys".toCharArray() - private const val AES_ALGORITHM = "AES" - private const val AES_GCM_TRANSFORM = "AES/GCM/NoPadding" - private const val AES_KEY_BITS = 256 - private const val GCM_TAG_BITS = 128 - private const val BYTE_MASK = 0xFF private const val SERIALIZED_LINE_COUNT = 3 } } diff --git a/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/JvmFileServiceTest.kt b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/JvmFileServiceTest.kt new file mode 100644 index 0000000000..90ede811be --- /dev/null +++ b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/JvmFileServiceTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.di.CoroutineDispatchers +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JvmFileServiceTest { + private lateinit var dir: File + + @BeforeTest + fun setUp() { + dir = Files.createTempDirectory("jvm-file-service-test").toFile() + } + + @AfterTest + fun tearDown() { + dir.deleteRecursively() + } + + @Test + fun `a file URI from the save dialog is written at its own path`() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + val service = JvmFileService(CoroutineDispatchers(dispatcher, dispatcher, dispatcher)) + val target = File(dir, "export.txt") + + val written = service.write(CommonUri.parse(target.toURI().toString())) { it.writeUtf8("hello") } + + assertTrue(written) + assertEquals("hello", target.readText()) + } + + @Test + fun `a file URI is read from its own path`() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + val service = JvmFileService(CoroutineDispatchers(dispatcher, dispatcher, dispatcher)) + val source = File(dir, "profile.cfg").apply { writeText("config") } + var text: String? = null + + val read = service.read(CommonUri.parse(source.toURI().toString())) { text = it.readUtf8() } + + assertTrue(read) + assertEquals("config", text) + } +} diff --git a/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/KeystoreFixtures.kt b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/KeystoreFixtures.kt new file mode 100644 index 0000000000..e451925f95 --- /dev/null +++ b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/KeystoreFixtures.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import java.io.File +import java.security.KeyStore +import kotlin.test.assertTrue + +/** + * Deletes [alias] from the store's `keystore.p12`, leaving the keystore file in place. Loading with the store's own + * [password] and asserting the alias existed keeps a stale literal from turning the caller into a no-op. + */ +internal fun removeKeystoreEntry(storeDir: File, alias: String, password: String) { + val file = File(storeDir, "keystore.p12") + val keyStore = KeyStore.getInstance("PKCS12") + file.inputStream().use { keyStore.load(it, password.toCharArray()) } + assertTrue(keyStore.containsAlias(alias), "fixture expects $alias in ${file.name}") + keyStore.deleteEntry(alias) + file.outputStream().use { keyStore.store(it, password.toCharArray()) } +} diff --git a/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImplTest.kt b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImplTest.kt index fa02d1350f..60fba18b92 100644 --- a/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImplTest.kt +++ b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImplTest.kt @@ -21,29 +21,27 @@ import java.nio.file.Files import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull class LockdownPassphraseStoreImplTest { - private lateinit var tempHome: java.nio.file.Path - private lateinit var originalUserHome: String + private lateinit var dataDir: File @BeforeTest fun setUp() { - originalUserHome = System.getProperty("user.home") - tempHome = Files.createTempDirectory("lockdown-passphrase-store-test") - System.setProperty("user.home", tempHome.toString()) + dataDir = Files.createTempDirectory("lockdown-passphrase-store-test").toFile() } @AfterTest fun tearDown() { - System.setProperty("user.home", originalUserHome) - File(tempHome.toString()).deleteRecursively() + dataDir.deleteRecursively() } @Test fun `save get and clear passphrase round trips on jvm`() { - val store = LockdownPassphraseStoreImpl() + val store = LockdownPassphraseStoreImpl(dataDir) store.savePassphrase(deviceAddress = "AA:BB:CC:DD", passphrase = "secret", boots = 10, hours = 24) @@ -56,4 +54,20 @@ class LockdownPassphraseStoreImplTest { assertNull(store.getPassphrase("AA:BB:CC:DD")) } + + @Test + fun `a keystore missing its master key is never overwritten`() { + LockdownPassphraseStoreImpl(dataDir).savePassphrase("AA:BB:CC:DD", "secret", boots = 10, hours = 24) + val storeDir = File(dataDir, "lockdown") + removeKeystoreEntry(storeDir, alias = "lockdown_master", password = "meshtastic-lockdown") + val keystoreBefore = File(storeDir, "keystore.p12").readBytes() + val entryBefore = File(storeDir, "AA_BB_CC_DD.enc").readBytes() + + val reopened = LockdownPassphraseStoreImpl(dataDir) + + assertNull(reopened.getPassphrase("AA:BB:CC:DD")) + assertFailsWith { reopened.savePassphrase("EE:FF", "other", boots = 1, hours = 1) } + assertContentEquals(keystoreBefore, File(storeDir, "keystore.p12").readBytes()) + assertContentEquals(entryBefore, File(storeDir, "AA_BB_CC_DD.enc").readBytes()) + } } diff --git a/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImplTest.kt b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImplTest.kt new file mode 100644 index 0000000000..5c66f75b35 --- /dev/null +++ b/core/service/src/jvmTest/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImplTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.service + +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +class SecurityKeyBackupStoreImplTest { + private lateinit var dataDir: File + + @BeforeTest + fun setUp() { + dataDir = Files.createTempDirectory("security-key-backup-store-test").toFile() + } + + @AfterTest + fun tearDown() { + dataDir.deleteRecursively() + } + + @Test + fun `save get and delete round trips on jvm`() { + val store = SecurityKeyBackupStoreImpl(dataDir) + + store.save(nodeNum = 42, publicKeyBase64 = "pub", privateKeyBase64 = "priv", timestamp = 1_700_000_000L) + + val stored = store.get(42) + assertEquals("pub", stored?.publicKeyBase64) + assertEquals("priv", stored?.privateKeyBase64) + assertEquals(1_700_000_000L, stored?.timestamp) + + store.delete(42) + + assertNull(store.get(42)) + } + + @Test + fun `a keystore missing its master key is never overwritten`() { + SecurityKeyBackupStoreImpl(dataDir).save(42, "pub", "priv", timestamp = 1L) + val storeDir = File(dataDir, "security_keys") + removeKeystoreEntry(storeDir, alias = "security_key_backup_master", password = "meshtastic-security-keys") + val keystoreBefore = File(storeDir, "keystore.p12").readBytes() + val entryBefore = File(storeDir, "42.enc").readBytes() + + val reopened = SecurityKeyBackupStoreImpl(dataDir) + + assertNull(reopened.get(42)) + assertFailsWith { reopened.save(7, "pub7", "priv7", timestamp = 2L) } + assertContentEquals(keystoreBefore, File(storeDir, "keystore.p12").readBytes()) + assertContentEquals(entryBefore, File(storeDir, "42.enc").readBytes()) + } +} diff --git a/core/takserver/build.gradle.kts b/core/takserver/build.gradle.kts index 93f435b2ea..a7247e9e5f 100644 --- a/core/takserver/build.gradle.kts +++ b/core/takserver/build.gradle.kts @@ -26,6 +26,8 @@ kotlin { @Suppress("UnstableApiUsage") android { withHostTest { isIncludeAndroidResources = true } } + compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } + sourceSets { commonMain.dependencies { api(projects.core.repository) @@ -45,6 +47,7 @@ kotlin { // are no native zstd-jni/xpp3 deps to re-add per target. implementation(libs.okio) + implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.serialization.json) implementation(libs.xmlutil.core) implementation(libs.xmlutil.serialization) diff --git a/core/takserver/detekt-baseline.xml b/core/takserver/detekt-baseline.xml index 0141557fb2..e0ff8ce2e0 100644 --- a/core/takserver/detekt-baseline.xml +++ b/core/takserver/detekt-baseline.xml @@ -2,7 +2,16 @@ - LongMethod:ZipArchiver.kt:ZipArchiver$actual fun createZip: ByteArray + AbstractClassCanBeInterface:TAKModels.kt:TAKConnectionEvent$TAKConnectionEvent + NoNameShadowing:TAKPacketV2Conversion.kt:TAKPacketV2Conversion$wb + NoNameShadowing:TakV2Compressor.kt:TakV2Compressor$wb ReturnCount:TAKServerJvm.kt:TAKServerJvm$override suspend fun start: Result<Unit> + UnusedPrivateProperty:TAKClientConnection.kt:TAKClientConnection$private val scope: CoroutineScope + UseOrEmpty:CoTXml.kt:chat.senderCallsign?.xmlEscaped() ?: "" + UseOrEmpty:CoTXmlParser.kt:CoTXmlParser$detail.remarks?.value ?: "" + UseOrEmpty:TAKPacketConversion.kt:TAKPacketConversion$it.endpoint ?: "" + UseOrEmpty:TAKPacketV2Conversion.kt:TAKPacketV2Conversion$contact?.endpoint ?: "" + UseOrEmpty:TakV2Compressor.kt:TakV2Compressor$data.cotTypeStr ?: "" + UseOrEmpty:TakV2Compressor.kt:TakV2Compressor$packet.marti?.dest_callsign?.toList() ?: emptyList() diff --git a/core/takserver/src/androidHostTest/kotlin/org/meshtastic/core/takserver/AtakFileWriterTest.kt b/core/takserver/src/androidHostTest/kotlin/org/meshtastic/core/takserver/AtakFileWriterTest.kt new file mode 100644 index 0000000000..62ab7cd63a --- /dev/null +++ b/core/takserver/src/androidHostTest/kotlin/org/meshtastic/core/takserver/AtakFileWriterTest.kt @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +import android.app.Application +import android.content.ContentProvider +import android.content.ContentUris +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.Environment +import android.os.ParcelFileDescriptor +import android.provider.BaseColumns +import android.provider.MediaStore +import co.touchlab.kermit.Severity +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.meshtastic.core.common.ContextServices +import org.meshtastic.core.testing.CapturingLogWriter +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.io.File +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +class AtakFileWriterTest { + + private val app: Application = RuntimeEnvironment.getApplication() + private lateinit var logs: CapturingLogWriter + + @Before + fun setUp() { + ContextServices.app = app + logs = CapturingLogWriter.install() + } + + @After + fun tearDown() { + CapturingLogWriter.uninstall() + } + + @Test + @Config(sdk = [34]) + fun `saves to the shared Downloads folder through MediaStore`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1, 2, 3))) + + val row = mediaStore.rows.values.single() + assertEquals("route-1.zip", row.values.getAsString(MediaStore.MediaColumns.DISPLAY_NAME)) + assertEquals("Download/", row.values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH)) + assertEquals(0, row.values.getAsInteger(MediaStore.MediaColumns.IS_PENDING)) + assertContentEquals(byteArrayOf(1, 2, 3), row.file.readBytes()) + } + + @Test + @Config(sdk = [34]) + fun `saving the same route again replaces the earlier file`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1, 2, 3))) + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(9))) + + val row = mediaStore.rows.values.single() + assertContentEquals(byteArrayOf(9), row.file.readBytes()) + } + + @Test + @Config(sdk = [34]) + fun `after a reinstall every update of a route replaces one new copy`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1))) + + mediaStore.reinstall() + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(2))) + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(3))) + + val names = mediaStore.rows.values.map { it.values.getAsString(MediaStore.MediaColumns.DISPLAY_NAME) } + assertEquals(listOf("route-1.zip", "route-1 (1).zip"), names) + assertContentEquals(byteArrayOf(3), mediaStore.rows.values.last().file.readBytes()) + } + + @Test + @Config(sdk = [34]) + fun `a route file saved without a marker is still replaced in place`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + app.contentResolver.insert( + MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), + ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, "route-1.zip") + put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/") + }, + ) + + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(9))) + + assertContentEquals(byteArrayOf(9), mediaStore.rows.values.single().file.readBytes()) + } + + @Test + @Config(sdk = [34]) + fun `a failed save removes its pending row`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + mediaStore.failUpdatesWith = IllegalStateException("provider refused the update") + + assertFalse(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1, 2, 3))) + + assertTrue(mediaStore.rows.isEmpty(), "pending rows left behind: ${mediaStore.rows.keys}") + } + + @Test + @Config(sdk = [34]) + fun `a failed save is logged as an error without the file name`() { + val mediaStore = Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + mediaStore.failUpdatesWith = IllegalStateException("provider refused the update") + + assertFalse(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1, 2, 3))) + + assertEquals(1, logs.messages(Severity.Error).size, "error logs: ${logs.messages(Severity.Error)}") + logs.assertNotLogged("route-1") + } + + @Test + @Config(sdk = [28]) + fun `a failed save below API 29 logs neither the file name nor its path`() { + val dir = checkNotNull(app.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)) + // A directory in the file's place makes the write fail with an error that names the full path. + check(File(dir, "route-1.zip").mkdirs()) + + assertFalse(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(5))) + + assertEquals(1, logs.messages(Severity.Error).size, "error logs: ${logs.messages(Severity.Error)}") + logs.assertNotLogged("route-1", dir.absolutePath) + } + + @Test + @Config(sdk = [34]) + fun `a successful save logs no error`() { + Robolectric.setupContentProvider(FakeMediaStore::class.java, MediaStore.AUTHORITY) + + assertTrue(AtakFileWriter.writeToImportDir("route-1.zip", byteArrayOf(1, 2, 3))) + + assertEquals(emptyList(), logs.messages(Severity.Error)) + } + + @Test + @Config(sdk = [28]) + fun `saves to the app external Downloads folder below API 29`() { + assertTrue(AtakFileWriter.writeToImportDir("route/../1.zip", byteArrayOf(5))) + + val dir = checkNotNull(app.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)) + assertContentEquals(byteArrayOf(5), File(dir, "route_.._1.zip").readBytes()) + } + + /** + * Just enough of MediaStore for the writer: rows keyed by id, each backed by a real file. Like MediaProvider, a + * query sees only published rows this install owns, and an insert renames a name already taken in its folder. + */ + class FakeMediaStore : ContentProvider() { + class Row(val values: ContentValues, val file: File) + + /** Every row in the collection, including ones an earlier install left behind. */ + val rows = linkedMapOf() + var failUpdatesWith: RuntimeException? = null + private var nextId = 1L + + /** An uninstall leaves the app's Downloads files in place but drops its ownership of them. */ + fun reinstall() = rows.values.forEach { it.values.putNull(MediaStore.MediaColumns.OWNER_PACKAGE_NAME) } + + override fun onCreate(): Boolean = true + + override fun getType(uri: Uri): String? = null + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?, + ): Cursor { + val visible = rows.filterValues { + it.isOwned() && it.values.getAsInteger(MediaStore.MediaColumns.IS_PENDING) != 1 + } + // SQLite evaluates the writer's real selection against the visible rows. + SQLiteDatabase.create(null).use { db -> + db.execSQL("CREATE TABLE files (${BaseColumns._ID} INTEGER PRIMARY KEY, ${COLUMNS.joinToString()})") + visible.forEach { (id, row) -> + db.insertOrThrow("files", null, ContentValues(row.values).apply { put(BaseColumns._ID, id) }) + } + db.query("files", projection, selection, selectionArgs, null, null, sortOrder).use { found -> + val cursor = MatrixCursor(found.columnNames) + while (found.moveToNext()) cursor.addRow(Array(found.columnCount) { found.getString(it) }) + return cursor + } + } + } + + override fun insert(uri: Uri, values: ContentValues?): Uri { + val id = nextId++ + val row = + ContentValues(values).apply { + put( + MediaStore.MediaColumns.DISPLAY_NAME, + uniqueName( + getAsString(MediaStore.MediaColumns.DISPLAY_NAME), + getAsString(MediaStore.MediaColumns.RELATIVE_PATH), + ), + ) + put(MediaStore.MediaColumns.OWNER_PACKAGE_NAME, checkNotNull(context).packageName) + } + val file = File.createTempFile("media", ".bin", checkNotNull(context).cacheDir) + rows[id] = Row(row, file) + return ContentUris.withAppendedId(uri, id) + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array?, + ): Int { + failUpdatesWith?.let { throw it } + val row = rows[ContentUris.parseId(uri)] ?: return 0 + row.values.putAll(values) + return 1 + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = + if (rows.remove(ContentUris.parseId(uri)) != null) 1 else 0 + + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor { + val row = rows.getValue(ContentUris.parseId(uri)) + if (!row.isOwned()) throw SecurityException("$uri is not owned by this app") + return ParcelFileDescriptor.open(row.file, ParcelFileDescriptor.parseMode(mode)) + } + + private fun Row.isOwned() = + values.getAsString(MediaStore.MediaColumns.OWNER_PACKAGE_NAME) == checkNotNull(context).packageName + + private fun uniqueName(name: String, relativePath: String?): String { + val taken = + rows.values + .filter { it.values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH) == relativePath } + .map { it.values.getAsString(MediaStore.MediaColumns.DISPLAY_NAME) } + .toSet() + val stem = name.substringBeforeLast('.') + val extension = name.substring(stem.length) + return generateSequence(0) { it + 1 } + .map { if (it == 0) name else "$stem ($it)$extension" } + .first { it !in taken } + } + + private companion object { + val COLUMNS = + listOf( + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.MIME_TYPE, + MediaStore.MediaColumns.RELATIVE_PATH, + MediaStore.MediaColumns.IS_PENDING, + MediaStore.MediaColumns.OWNER_PACKAGE_NAME, + MediaStore.DownloadColumns.DOWNLOAD_URI, + ) + } + } +} diff --git a/core/takserver/src/androidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt b/core/takserver/src/androidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt index 9abd1017ed..df610159d1 100644 --- a/core/takserver/src/androidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt +++ b/core/takserver/src/androidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -16,38 +16,109 @@ */ package org.meshtastic.core.takserver +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi import co.touchlab.kermit.Logger +import org.meshtastic.core.common.ContextServices import java.io.File +import java.io.IOException -/** - * Android implementation — writes route data packages to ATAK's monitored auto-import directory. Tries multiple - * locations in order of preference: - * 1. `/sdcard/atak/tools/datapackage/` (ATAK monitors this) - * 2. `/sdcard/Download/` (user can manually import from here) - */ -@Suppress("TooGenericExceptionCaught") internal actual object AtakFileWriter { + @Suppress("TooGenericExceptionCaught") actual fun writeToImportDir(fileName: String, zipBytes: ByteArray): Boolean { // Sanitize: fileName originates from untrusted mesh CoT uid attributes. - val safeName = fileName.replace(Regex("[^a-zA-Z0-9._-]"), "_") - // Use hardcoded paths — on Android /sdcard/ maps to external storage. - // On JVM desktop these paths don't exist and the fallback returns false. - val targets = listOf(File("/sdcard/atak/tools/datapackage"), File("/sdcard/Download")) + val safeName = fileName.replace(UNSAFE_FILE_NAME_CHARS, "_") + return try { + val context = ContextServices.app + val location = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + writeToSharedDownloads(context, safeName, zipBytes).toString() + } else { + // Shared storage needs WRITE_EXTERNAL_STORAGE below API 29; the app's own external dir needs none. + writeToAppExternalDownloads(context, safeName, zipBytes) + } + Logger.i { "Route data package written: $safeName (${zipBytes.size} bytes) to $location" } + true + } catch (e: Exception) { + // No throwable: platform file errors carry the path, and the name comes from mesh data. + Logger.e { "Route data package was not saved: ${e::class.simpleName}" } + false + } + } - for (dir in targets) { - try { - if (!dir.exists()) dir.mkdirs() - val target = File(dir, safeName) - target.writeBytes(zipBytes) - Logger.i { "Route data package written: $fileName (${zipBytes.size} bytes) → ${target.absolutePath}" } - return true - } catch (e: Exception) { - Logger.d { "Cannot write to ${dir.absolutePath}: ${e.message}" } - } + /** + * Route updates overwrite the row this install saved, found by its [MediaStore.Downloads.DOWNLOAD_URI] marker + * because a reinstall orphans the old row and MediaProvider renames the new one. Rows saved before the marker match + * by name. + */ + @RequiresApi(Build.VERSION_CODES.Q) + private fun writeToSharedDownloads(context: Context, name: String, bytes: ByteArray): Uri { + val resolver = context.contentResolver + val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val marker = ROUTE_MARKER_PREFIX + name + val existing = + resolver + .query( + collection, + arrayOf(MediaStore.Downloads._ID), + "${MediaStore.Downloads.RELATIVE_PATH} = ? AND " + + "(${MediaStore.Downloads.DOWNLOAD_URI} = ? OR ${MediaStore.Downloads.DISPLAY_NAME} = ?)", + arrayOf(DOWNLOADS_RELATIVE_PATH, marker, name), + null, + ) + ?.use { cursor -> + if (cursor.moveToFirst()) ContentUris.withAppendedId(collection, cursor.getLong(0)) else null + } + if (existing != null) { + resolver.writeBytes(existing, "wt", bytes) + return existing } - Logger.w { "Failed to write route data package to any ATAK import directory" } - return false + val pending = + ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, name) + put(MediaStore.Downloads.MIME_TYPE, ZIP_MIME_TYPE) + put(MediaStore.Downloads.RELATIVE_PATH, DOWNLOADS_RELATIVE_PATH) + put(MediaStore.Downloads.DOWNLOAD_URI, marker) + put(MediaStore.Downloads.IS_PENDING, 1) + } + val inserted = resolver.insert(collection, pending) ?: throw IOException("MediaStore refused to create $name") + // A pending row left behind hides this name from the lookup above, so a retry would get a renamed copy. + var published = false + try { + resolver.writeBytes(inserted, "w", bytes) + resolver.update(inserted, ContentValues().apply { put(MediaStore.Downloads.IS_PENDING, 0) }, null, null) + published = true + } finally { + if (!published) resolver.delete(inserted, null, null) + } + return inserted } + + private fun writeToAppExternalDownloads(context: Context, name: String, bytes: ByteArray): String { + val dir = + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + ?: throw IOException("External storage is not available") + val target = File(dir, name) + target.writeBytes(bytes) + return target.absolutePath + } + + private fun ContentResolver.writeBytes(uri: Uri, mode: String, bytes: ByteArray) { + val stream = openOutputStream(uri, mode) ?: throw IOException("No output stream for $uri") + stream.use { it.write(bytes) } + } + + private val UNSAFE_FILE_NAME_CHARS = Regex("[^a-zA-Z0-9._-]") + private val DOWNLOADS_RELATIVE_PATH = "${Environment.DIRECTORY_DOWNLOADS}/" + private const val ZIP_MIME_TYPE = "application/zip" + private const val ROUTE_MARKER_PREFIX = "meshtastic://atak-route/" } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt index 4d863e14fe..5afb924b2d 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -17,14 +17,14 @@ package org.meshtastic.core.takserver /** - * Writes data package files to ATAK's auto-import directory. + * Saves data package files where the user can import them into ATAK. * - * On Android, the actual implementation writes to `/sdcard/atak/tools/datapackage/` which ATAK monitors for new zip - * files. On other platforms this is a no-op. + * On Android the package goes to the shared Downloads folder (the app's own external Downloads folder below API 29), + * without any storage permission. On other platforms this is a no-op. */ internal expect object AtakFileWriter { /** - * Write a data package zip to ATAK's monitored import directory. + * Save a data package zip, replacing the one this install saved earlier under the same name. * * @return true if the file was written successfully, false otherwise. */ diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTConversion.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTConversion.kt index 319154974c..dcc53e34d3 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTConversion.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTConversion.kt @@ -30,7 +30,8 @@ fun org.meshtastic.proto.Position.toCoTMessage( ): CoTMessage { val lat = (latitude_i ?: 0).toDouble() / TAK_COORDINATE_SCALE val lon = (longitude_i ?: 0).toDouble() / TAK_COORDINATE_SCALE - val altitude = (altitude ?: 0).toDouble() + // CoT marks an unknown height with its sentinel; 0 would place the node at sea level. + val altitude = altitude?.toDouble() ?: TAK_UNKNOWN_POINT_VALUE val speed = (ground_speed ?: 0).toDouble() val course = (ground_track ?: 0).toDouble() diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt index e98dde4a13..eaa5047d46 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt @@ -28,20 +28,21 @@ import kotlin.time.Instant // wire format we exchange with ATAK/TAK servers. Staying on the compat policy until that migration can // be validated against real TAK interop; suppress the soft-deprecation on the factory itself. @Suppress("DEPRECATION") -private val xmlParser = - XML.compat { - // xmlutil 1.0.0 moved repairNamespaces from the policy builder to the top-level XML config. - repairNamespaces = false - defaultPolicy { ignoreUnknownChildren() } - } +private val xmlParser = XML.compat { + // xmlutil 1.0.0 moved repairNamespaces from the policy builder to the top-level XML config. + repairNamespaces = false + defaultPolicy { ignoreUnknownChildren() } +} + +/** Fractional seconds, stripped for a second parse attempt of a CoT timestamp ISO 8601 parsing rejected. */ +internal val FRACTIONAL_SECONDS = Regex("""\.\d+""") class CoTXmlParser(private val xml: String) { fun parse(): Result = try { val event = xmlParser.decodeFromString(CoTEventXml.serializer(), xml) Result.success(buildCoTMessage(event)) } catch (e: IllegalArgumentException) { - Result.failure(e) - } catch (e: kotlinx.serialization.SerializationException) { + // Also covers SerializationException, which extends it. Result.failure(e) } catch (e: nl.adaptivity.xmlutil.XmlException) { Result.failure(e) @@ -136,16 +137,8 @@ class CoTXmlParser(private val xml: String) { private fun parseDate(dateString: String?): Instant { if (dateString.isNullOrEmpty()) return Clock.System.now() - return try { - Instant.parse(dateString) - } catch (ignored: IllegalArgumentException) { - try { - val cleaned = dateString.replace(Regex("""\.\d+"""), "").replace("Z", "+00:00") - Instant.parse(cleaned) - } catch (ignoredInner: IllegalArgumentException) { - Logger.w { "Unparseable CoT date '$dateString', falling back to now()" } - Clock.System.now() - } - } + return Instant.parseOrNull(dateString) + ?: Instant.parseOrNull(dateString.replace(FRACTIONAL_SECONDS, "").replace("Z", "+00:00")) + ?: Clock.System.now().also { Logger.w { "Unparseable CoT date '$dateString', falling back to now()" } } } } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcaster.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcaster.kt index c78c9ec55e..0b96a364a9 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcaster.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcaster.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.takserver import co.touchlab.kermit.Logger +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope @@ -30,8 +31,6 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.TakPrefs import kotlin.concurrent.Volatile -import kotlin.concurrent.atomics.AtomicBoolean -import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.Instant /** @@ -43,14 +42,13 @@ import kotlin.time.Instant * Opt-in via [TakPrefs.isMeshToCotEnabled] and additionally gated on the TAK server running, because the owning * [TAKMeshIntegration] is itself only started while the server is enabled. */ -@OptIn(ExperimentalAtomicApi::class) class MeshToCotBroadcaster( private val takServerManager: TAKServerManager, private val nodeRepository: NodeRepository, private val takPrefs: TakPrefs, private val dispatchers: CoroutineDispatchers, ) { - private val isRunning = AtomicBoolean(false) + private val isRunning = atomic(false) @Volatile private var job: Job? = null @@ -62,7 +60,7 @@ class MeshToCotBroadcaster( fun start(scope: CoroutineScope) { // CAS, not a job-null check: two concurrent start() calls must not both launch, and a // dead job left by a cancelled scope must not block every future start(). - if (!isRunning.compareAndSet(expectedValue = false, newValue = true)) return + if (!isRunning.compareAndSet(expect = false, update = true)) return job = scope.launch(dispatchers.default) { try { @@ -74,13 +72,13 @@ class MeshToCotBroadcaster( } finally { // Owning scope cancelled without stop(): release the guard so a later start() // on a fresh scope isn't refused forever. - isRunning.store(false) + isRunning.value = false } } } fun stop() { - if (!isRunning.compareAndSet(expectedValue = true, newValue = false)) return + if (!isRunning.compareAndSet(expect = true, update = false)) return // lastSent is deliberately NOT cleared here: cancel() doesn't join, so a still-finishing // publish() on another thread may hold sentMutex, and clearing unsynchronized would race // it. runEnabled() drops the state on the next enable instead. diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt index a8b34e9e0c..c27ac6f136 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt @@ -19,6 +19,8 @@ package org.meshtastic.core.takserver import co.touchlab.kermit.Logger +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.distinctUntilChanged @@ -43,8 +45,6 @@ import org.meshtastic.proto.PortNum import org.meshtastic.proto.TAKPacket import org.meshtastic.proto.Team import kotlin.concurrent.Volatile -import kotlin.concurrent.atomics.AtomicBoolean -import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.Clock import kotlin.time.Duration.Companion.minutes @@ -88,7 +88,6 @@ internal sealed interface TakSendOutcome { * regardless of the local radio's firmware version, so a v2-capable node can still relay legacy v1 packets received * from older nodes in mixed-firmware mesh deployments. */ -@OptIn(ExperimentalAtomicApi::class) @Suppress("TooManyFunctions") class TAKMeshIntegration( private val takServerManager: TAKServerManager, @@ -99,7 +98,7 @@ class TAKMeshIntegration( private val meshToCotBroadcaster: MeshToCotBroadcaster, private val takPrefs: TakPrefs, ) { - private val isRunning = AtomicBoolean(false) + private val isRunning = atomic(false) // Immutable list reference replaced atomically in start()/stop(); never mutated in-place. // @Volatile only guarantees visibility of the reference itself — any in-place mutation @@ -116,7 +115,7 @@ class TAKMeshIntegration( private val deliveryDedup = CotDeliveryDedup() fun start(scope: CoroutineScope) { - if (!isRunning.compareAndSet(expectedValue = false, newValue = true)) return + if (!isRunning.compareAndSet(expect = false, update = true)) return takServerManager.start(scope) @@ -179,7 +178,7 @@ class TAKMeshIntegration( } fun stop() { - if (!isRunning.compareAndSet(expectedValue = true, newValue = false)) return + if (!isRunning.compareAndSet(expect = true, update = false)) return val toCancel = jobs jobs = emptyList() toCancel.forEach(Job::cancel) @@ -306,7 +305,7 @@ class TAKMeshIntegration( commandSender.sendData(dataPacket) Logger.d { "Sent V2 to mesh: ${cotMessage.type} (${wirePayload.size} bytes)" } TakSendOutcome.Sent(wirePayload.size) - } catch (e: kotlin.coroutines.cancellation.CancellationException) { + } catch (e: CancellationException) { throw e } catch (e: Exception) { // Something other than size — radio not connected, queue full, etc. @@ -357,7 +356,7 @@ class TAKMeshIntegration( commandSender.sendData(dataPacket) Logger.d { "Sent V1 to mesh: ${cotMessage.type} (${wirePayload.size} bytes)" } TakSendOutcome.Sent(wirePayload.size) - } catch (e: kotlin.coroutines.cancellation.CancellationException) { + } catch (e: CancellationException) { throw e } catch (e: Exception) { Logger.e(e) { @@ -381,7 +380,7 @@ class TAKMeshIntegration( // ── Receive: mesh → TAK client ────────────────────────────────────────── - private suspend fun handleMeshPacket(packet: MeshPacket) { + private fun handleMeshPacket(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return when (packet.decoded?.portnum) { @@ -391,7 +390,7 @@ class TAKMeshIntegration( } } - private suspend fun handleV2Packet(wirePayload: ByteArray) { + private fun handleV2Packet(wirePayload: ByteArray) { try { // Decompress to CoT XML via the SDK's CotXmlBuilder, which handles // ALL typed payloads (DrawnShape, Marker, Route, etc.) and preserves @@ -412,7 +411,7 @@ class TAKMeshIntegration( } // Logger.d { "RAW CoT IN (mesh): $xml" } // Routes: ATAK ignores b-m-r CoT events over TCP streaming. - // Convert to a KML data package and write to ATAK's auto-import dir. + // Convert to a KML data package and save it to Downloads for import into ATAK. if (xml.contains("""type="b-m-r"""")) { try { val pkg = RouteDataPackageGenerator.generateDataPackage(xml) @@ -442,7 +441,7 @@ class TAKMeshIntegration( * Packets flagged `is_compressed` are skipped only when the local radio is 2.7.x — that firmware, and only that * firmware, also delivers a decompressed copy. See the inline comment for the details. */ - private suspend fun handleV1Packet(payload: okio.ByteString) { + private fun handleV1Packet(payload: okio.ByteString) { try { val takPacket = TAKPacket.ADAPTER.decode(payload) // A *local* 2.7.x radio unishox2-decompresses inbound port 72 traffic into a copy and sends that to the @@ -491,17 +490,10 @@ class TAKMeshIntegration( val staleInTag = STALE_ATTR_RE.find(eventTag) ?: return xml val staleStr = staleInTag.groupValues[1] val staleInstant = - try { - kotlin.time.Instant.parse(staleStr) - } catch (_: IllegalArgumentException) { + kotlin.time.Instant.parseOrNull(staleStr) // Handle edge-case formats like missing "Z" - try { - val cleaned = staleStr.replace(Regex("""\.\d+"""), "").replace("Z", "+00:00") - kotlin.time.Instant.parse(cleaned) - } catch (_: IllegalArgumentException) { - return xml - } - } + ?: kotlin.time.Instant.parseOrNull(staleStr.replace(FRACTIONAL_SECONDS, "").replace("Z", "+00:00")) + ?: return xml val now = Clock.System.now() val remaining = staleInstant - now diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt index a53571db89..d2737aaa3d 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.takserver import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -232,7 +233,7 @@ class TakMeshTestRunner(private val takMeshIntegration: TAKMeshIntegration) { TakTestResult(name, xml.length, 0, false, outcome.reason, protocol) } } - } catch (e: kotlin.coroutines.cancellation.CancellationException) { + } catch (e: CancellationException) { throw e } catch (e: Exception) { Logger.w(e) { "TAK Test: $name send failed: ${e.message}" } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt index f00f9863ac..121ecdd425 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt @@ -92,133 +92,142 @@ internal object TakV2Compressor { val cotTypeId = packet.cot_type_id.value val cotTypeStr = if (cotTypeId == 0 && packet.cot_type_str.isNotEmpty()) packet.cot_type_str else null + // Wire's generated fields live in another module, so only locals smart-cast after a null check. + val chat = packet.chat + val taktalk = packet.taktalk + val taktalkRoom = packet.taktalk_room + val aircraft = packet.aircraft + val shape = packet.shape + val marker = packet.marker + val rab = packet.rab + val route = packet.route + val casevac = packet.casevac + val emergency = packet.emergency + val task = packet.task + val rawDetail = packet.raw_detail val payload = when { - packet.chat != null -> + chat != null -> TakPacketV2Data.Payload.Chat( - message = packet.chat!!.message, - to = packet.chat!!.to, - toCallsign = packet.chat!!.to_callsign, - receiptForUid = packet.chat!!.receipt_for_uid, - receiptType = packet.chat!!.receipt_type.value, + message = chat.message, + to = chat.to, + toCallsign = chat.to_callsign, + receiptForUid = chat.receipt_for_uid, + receiptType = chat.receipt_type.value, // TAKTALK sidecars (proto3 optional → wire nullable). // Empty string = empty `` / `` in source XML; // null on the wire = field absent. The SDK's Chat data class // uses "" for absent, so map null → "". voice_profile_id has // a present-vs-empty-marker distinction tracked separately // via hasVoiceProfile. - lang = packet.chat!!.lang ?: "", - roomId = packet.chat!!.room_id ?: "", - voiceProfileId = packet.chat!!.voice_profile_id ?: "", - hasVoiceProfile = packet.chat!!.voice_profile_id != null, + lang = chat.lang.orEmpty(), + roomId = chat.room_id.orEmpty(), + voiceProfileId = chat.voice_profile_id.orEmpty(), + hasVoiceProfile = chat.voice_profile_id != null, ) // TAKTALK voice/text message (m-t-t). Without this branch, // m-t-t events fall through to Payload.None and the receiver // can't rebuild the CoT event, so TTS playback never fires. - packet.taktalk != null -> + taktalk != null -> TakPacketV2Data.Payload.TakTalk( - text = packet.taktalk!!.text, - chatroomId = packet.taktalk!!.chatroom_id, - lang = packet.taktalk!!.lang, - fromVoice = packet.taktalk!!.from_voice, + text = taktalk.text, + chatroomId = taktalk.chatroom_id, + lang = taktalk.lang, + fromVoice = taktalk.from_voice, ) // TAKTALK room/membership broadcast (y-). - packet.taktalk_room != null -> + taktalkRoom != null -> TakPacketV2Data.Payload.TakTalkRoom( - roomId = packet.taktalk_room!!.room_id, - roomName = packet.taktalk_room!!.room_name, - participants = packet.taktalk_room!!.participants.toList(), + roomId = taktalkRoom.room_id, + roomName = taktalkRoom.room_name, + participants = taktalkRoom.participants.toList(), ) - packet.aircraft != null -> + aircraft != null -> TakPacketV2Data.Payload.Aircraft( - icao = packet.aircraft!!.icao, - registration = packet.aircraft!!.registration, - flight = packet.aircraft!!.flight, - aircraftType = packet.aircraft!!.aircraft_type, - squawk = packet.aircraft!!.squawk, - category = packet.aircraft!!.category, - rssiX10 = packet.aircraft!!.rssi_x10, - gps = packet.aircraft!!.gps, - cotHostId = packet.aircraft!!.cot_host_id, + icao = aircraft.icao, + registration = aircraft.registration, + flight = aircraft.flight, + aircraftType = aircraft.aircraft_type, + squawk = aircraft.squawk, + category = aircraft.category, + rssiX10 = aircraft.rssi_x10, + gps = aircraft.gps, + cotHostId = aircraft.cot_host_id, ) // Typed geometry variants added by takv2_geometry (tags 34-37). // All GeoPoint fields on the wire are delta-encoded from the // event anchor; the SDK data class stores absolute lat/lon, so // we add packet.latitude_i / longitude_i here. - packet.shape != null -> { - val s = packet.shape!! + shape != null -> { TakPacketV2Data.Payload.DrawnShape( - kind = s.kind.value, - style = s.style.value, - majorCm = s.major_cm, - minorCm = s.minor_cm, - angleDeg = s.angle_deg, - strokeColor = s.stroke_color.value, - strokeArgb = s.stroke_argb, - strokeWeightX10 = s.stroke_weight_x10, - fillColor = s.fill_color.value, - fillArgb = s.fill_argb, - labelsOn = s.labels_on, + kind = shape.kind.value, + style = shape.style.value, + majorCm = shape.major_cm, + minorCm = shape.minor_cm, + angleDeg = shape.angle_deg, + strokeColor = shape.stroke_color.value, + strokeArgb = shape.stroke_argb, + strokeWeightX10 = shape.stroke_weight_x10, + fillColor = shape.fill_color.value, + fillArgb = shape.fill_argb, + labelsOn = shape.labels_on, // v0.4.0: vertices are two packed sint32 delta columns // (vertex_lat_deltas / vertex_lon_deltas), zigzag deltas // from the event anchor; SDK data stores absolute lat/lon. vertices = - s.vertex_lat_deltas.zip(s.vertex_lon_deltas) { latD, lonD -> + shape.vertex_lat_deltas.zip(shape.vertex_lon_deltas) { latD, lonD -> TakPacketV2Data.Payload.Vertex( latI = packet.latitude_i + latD, lonI = packet.longitude_i + lonD, ) }, - truncated = s.truncated, - bullseyeDistanceDm = s.bullseye_distance_dm, - bullseyeBearingRef = s.bullseye_bearing_ref, - bullseyeFlags = s.bullseye_flags, - bullseyeUidRef = s.bullseye_uid_ref, + truncated = shape.truncated, + bullseyeDistanceDm = shape.bullseye_distance_dm, + bullseyeBearingRef = shape.bullseye_bearing_ref, + bullseyeFlags = shape.bullseye_flags, + bullseyeUidRef = shape.bullseye_uid_ref, ) } - packet.marker != null -> { - val m = packet.marker!! + marker != null -> { TakPacketV2Data.Payload.Marker( - kind = m.kind.value, - color = m.color.value, - colorArgb = m.color_argb, - readiness = m.readiness, - parentUid = m.parent_uid, - parentType = m.parent_type, - parentCallsign = m.parent_callsign, - iconset = m.iconset, + kind = marker.kind.value, + color = marker.color.value, + colorArgb = marker.color_argb, + readiness = marker.readiness, + parentUid = marker.parent_uid, + parentType = marker.parent_type, + parentCallsign = marker.parent_callsign, + iconset = marker.iconset, ) } - packet.rab != null -> { - val r = packet.rab!! - val anchor = r.anchor + rab != null -> { + val anchor = rab.anchor TakPacketV2Data.Payload.RangeAndBearing( anchorLatI = packet.latitude_i + (anchor?.lat_delta_i ?: 0), anchorLonI = packet.longitude_i + (anchor?.lon_delta_i ?: 0), - anchorUid = r.anchor_uid, - rangeCm = r.range_cm, - bearingCdeg = r.bearing_cdeg, - strokeColor = r.stroke_color.value, - strokeArgb = r.stroke_argb, - strokeWeightX10 = r.stroke_weight_x10, + anchorUid = rab.anchor_uid, + rangeCm = rab.range_cm, + bearingCdeg = rab.bearing_cdeg, + strokeColor = rab.stroke_color.value, + strokeArgb = rab.stroke_argb, + strokeWeightX10 = rab.stroke_weight_x10, ) } - packet.route != null -> { - val rt = packet.route!! + route != null -> { TakPacketV2Data.Payload.Route( - method = rt.method.value, - direction = rt.direction.value, - prefix = rt.prefix, - strokeWeightX10 = rt.stroke_weight_x10, + method = route.method.value, + direction = route.direction.value, + prefix = route.prefix, + strokeWeightX10 = route.stroke_weight_x10, links = - rt.links.map { link -> + route.links.map { link -> val pt = link.point TakPacketV2Data.Payload.Route.Link( latI = packet.latitude_i + (pt?.lat_delta_i ?: 0), @@ -228,53 +237,50 @@ internal object TakV2Compressor { linkType = link.link_type, ) }, - truncated = rt.truncated, + truncated = route.truncated, ) } - packet.casevac != null -> { - val c = packet.casevac!! + casevac != null -> { TakPacketV2Data.Payload.CasevacReport( - precedence = c.precedence.value, - equipmentFlags = c.equipment_flags, - litterPatients = c.litter_patients, - ambulatoryPatients = c.ambulatory_patients, - security = c.security.value, - hlzMarking = c.hlz_marking.value, - zoneMarker = c.zone_marker, - usMilitary = c.us_military, - usCivilian = c.us_civilian, - nonUsMilitary = c.non_us_military, - nonUsCivilian = c.non_us_civilian, - epw = c.epw, - child = c.child, - terrainFlags = c.terrain_flags, - frequency = c.frequency, + precedence = casevac.precedence.value, + equipmentFlags = casevac.equipment_flags, + litterPatients = casevac.litter_patients, + ambulatoryPatients = casevac.ambulatory_patients, + security = casevac.security.value, + hlzMarking = casevac.hlz_marking.value, + zoneMarker = casevac.zone_marker, + usMilitary = casevac.us_military, + usCivilian = casevac.us_civilian, + nonUsMilitary = casevac.non_us_military, + nonUsCivilian = casevac.non_us_civilian, + epw = casevac.epw, + child = casevac.child, + terrainFlags = casevac.terrain_flags, + frequency = casevac.frequency, ) } - packet.emergency != null -> { - val e = packet.emergency!! + emergency != null -> { TakPacketV2Data.Payload.EmergencyAlert( - type = e.type.value, - authoringUid = e.authoring_uid, - cancelReferenceUid = e.cancel_reference_uid, + type = emergency.type.value, + authoringUid = emergency.authoring_uid, + cancelReferenceUid = emergency.cancel_reference_uid, ) } - packet.task != null -> { - val t = packet.task!! + task != null -> { TakPacketV2Data.Payload.TaskRequest( - taskType = t.task_type, - targetUid = t.target_uid, - assigneeUid = t.assignee_uid, - priority = t.priority.value, - status = t.status.value, - note = t.note, + taskType = task.task_type, + targetUid = task.target_uid, + assigneeUid = task.assignee_uid, + priority = task.priority.value, + status = task.status.value, + note = task.note, ) } - packet.raw_detail != null -> TakPacketV2Data.Payload.RawDetail(packet.raw_detail!!.toByteArray()) + rawDetail != null -> TakPacketV2Data.Payload.RawDetail(rawDetail.toByteArray()) // v0.4.0: PLI is implicit — a packet with no payload_variant set // is a position report (the bool pli oneof arm was removed). diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt index 9e592b1701..0e9a363765 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt @@ -60,6 +60,33 @@ class CoTConversionTest { assertEquals(85, cot.status?.battery) } + @Test + fun positionWithoutAltitudeSendsTheCotUnknownHeight() { + val position = + Position.Builder() + .also { wb -> + wb.latitude_i = 377749000 + wb.longitude_i = -1224194000 + } + .build() + + assertEquals(TAK_UNKNOWN_POINT_VALUE, position.toCoTMessage(uid = "!12345678", callsign = "TestUser").hae) + } + + @Test + fun seaLevelAltitudeStaysZero() { + val position = + Position.Builder() + .also { wb -> + wb.latitude_i = 377749000 + wb.longitude_i = -1224194000 + wb.altitude = 0 + } + .build() + + assertEquals(0.0, position.toCoTMessage(uid = "!12345678", callsign = "TestUser").hae) + } + @Test fun testUserToCoTMessage() { val user = diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt index b6c5cf9081..7d5f566f64 100644 --- a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt @@ -105,6 +105,8 @@ internal class TAKServerJvm(private val dispatchers: CoroutineDispatchers, priva acceptJob = scope.launch(dispatchers.io) { acceptLoop() } Result.success(Unit) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to bind TAK Server to 127.0.0.1:$port" } running = false @@ -230,13 +232,12 @@ internal class TAKServerJvm(private val dispatchers: CoroutineDispatchers, priva // Guard the snapshot+clear with the same lock used by the coroutine accept/disconnect // paths to avoid concurrent modification or a stale connectionCount during shutdown. - val toClose = - connectionsLock.withLock { - val snapshot = connections.values.toList() - connections.clear() - _connectionCount.value = 0 - snapshot - } + val toClose = connectionsLock.withLock { + val snapshot = connections.values.toList() + connections.clear() + _connectionCount.value = 0 + snapshot + } toClose.forEach { it.close() } try { diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakFixtureLoader.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakFixtureLoader.kt index 0822feff88..1ddc627aef 100644 --- a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakFixtureLoader.kt +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakFixtureLoader.kt @@ -20,6 +20,6 @@ package org.meshtastic.core.takserver internal actual fun loadTakFixtureXml(name: String): String { val stream = object {}::class.java.classLoader?.getResourceAsStream("tak_test_fixtures/$name") - ?: throw IllegalStateException("Fixture not found: tak_test_fixtures/$name") + ?: error("Fixture not found: tak_test_fixtures/$name") return stream.bufferedReader().readText() } diff --git a/core/takserver/src/jvmMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt b/core/takserver/src/jvmMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt index ec7f7727e1..6cc3faa52a 100644 --- a/core/takserver/src/jvmMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt +++ b/core/takserver/src/jvmMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -17,8 +17,8 @@ package org.meshtastic.core.takserver /** - * Desktop JVM no-op — writing data packages to ATAK's monitored directory is Android-only behaviour. On desktop, data - * packages are shared via the export launcher (file chooser) instead. + * Desktop JVM no-op: saving route data packages to Downloads is Android-only behaviour. On desktop, data packages are + * shared via the export launcher (file chooser) instead. */ internal actual object AtakFileWriter { actual fun writeToImportDir(fileName: String, zipBytes: ByteArray): Boolean = false diff --git a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt b/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt index 6a7c1bf697..3f1446b493 100644 --- a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt +++ b/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt @@ -20,6 +20,7 @@ import android.Manifest import android.app.Application import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager import android.content.Intent import android.os.Looper import org.robolectric.RuntimeEnvironment @@ -49,12 +50,9 @@ object RobolectricBleBonding { private val application: Application get() = RuntimeEnvironment.getApplication() - /** - * The default adapter Robolectric exposes; production resolves the same one via - * [android.bluetooth.BluetoothManager]. - */ - private val adapter: BluetoothAdapter - get() = BluetoothAdapter.getDefaultAdapter() + /** The adapter Robolectric exposes through [BluetoothManager], the same one production resolves. */ + val adapter: BluetoothAdapter + get() = application.getSystemService(BluetoothManager::class.java).adapter /** Grant the runtime permissions [ShadowBluetoothDevice.createBond] checks, so it returns instead of throwing. */ fun grantBluetoothConnectPermission() { diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/BaseFake.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/BaseFake.kt index f32eb99197..e35f7a0ca4 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/BaseFake.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/BaseFake.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow /** Base class for fakes that provides common utilities for state management and reset capabilities. */ +@Suppress("AbstractClassCanBeConcreteClass") // only ever extended, never a fake of its own abstract class BaseFake { private val resetActions = mutableListOf<() -> Unit>() diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/CapturingLogWriter.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/CapturingLogWriter.kt new file mode 100644 index 0000000000..d4b1db7417 --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/CapturingLogWriter.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.testing + +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.platformLogWriter +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * A Kermit writer that records every line, for tests that assert what was or was not logged. Hand it to + * `Logger(loggerConfigInit(writer))`, or [install] it on the global `Logger` and call [uninstall] in teardown. + */ +class CapturingLogWriter : LogWriter() { + data class Entry(val severity: Severity, val tag: String, val message: String, val throwable: Throwable?) + + private val recorded = mutableListOf() + + val entries: List + get() = recorded.toList() + + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + recorded += Entry(severity, tag, message, throwable) + } + + fun messages(severity: Severity? = null): List = + entries.filter { severity == null || it.severity == severity }.map { it.message } + + /** Asserts that something was logged and that no message or attached throwable text contains any of [values]. */ + fun assertNotLogged(vararg values: String) { + val text = entries.flatMap { listOfNotNull(it.message, it.throwable?.stackTraceToString()) } + assertTrue(text.isNotEmpty(), "Expected something to be logged") + for (value in values) { + assertFalse(text.any { value in it }, "'$value' leaked into logs: $text") + } + } + + companion object { + /** Replaces the global `Logger` writers with a new capturing writer that keeps every severity. */ + fun install(): CapturingLogWriter = CapturingLogWriter().also { + Logger.setLogWriters(it) + Logger.setMinSeverity(Severity.Verbose) + } + + /** Restores the platform writer on the global `Logger`. */ + fun uninstall() { + Logger.setLogWriters(platformLogWriter()) + } + } +} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt index c1ecc4ab0e..eb9cc7da36 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.update import org.meshtastic.core.model.DeviceType import org.meshtastic.core.repository.AnalyticsPrefs import org.meshtastic.core.repository.AppFunctionsPrefs +import org.meshtastic.core.repository.AppFunctionsSetting import org.meshtastic.core.repository.AppPreferences import org.meshtastic.core.repository.CustomEmojiPrefs import org.meshtastic.core.repository.FilterPrefs @@ -39,8 +40,8 @@ import org.meshtastic.core.repository.UiPrefs class FakeAnalyticsPrefs : AnalyticsPrefs { override val analyticsAllowed = MutableStateFlow(true) - override fun setAnalyticsAllowed(allowed: Boolean) { - analyticsAllowed.value = allowed + override fun toggleAnalyticsAllowed() { + analyticsAllowed.update { !it } } override val installId = MutableStateFlow("fake-install-id") @@ -49,8 +50,8 @@ class FakeAnalyticsPrefs : AnalyticsPrefs { class FakeHomoglyphPrefs : HomoglyphPrefs { override val homoglyphEncodingEnabled = MutableStateFlow(false) - override fun setHomoglyphEncodingEnabled(enabled: Boolean) { - homoglyphEncodingEnabled.value = enabled + override fun toggleHomoglyphEncodingEnabled() { + homoglyphEncodingEnabled.update { !it } } } @@ -129,8 +130,8 @@ class FakeUiPrefs : UiPrefs { override val showQuickChat = MutableStateFlow(true) - override fun setShowQuickChat(show: Boolean) { - showQuickChat.value = show + override fun toggleShowQuickChat() { + showQuickChat.update { !it } } override val showFullMessageTimestamps = MutableStateFlow(false) @@ -348,63 +349,31 @@ class FakeMeshPrefs : MeshPrefs { class FakeAppFunctionsPrefs : AppFunctionsPrefs { override val masterEnabled = MutableStateFlow(true) - - override fun setMasterEnabled(enabled: Boolean) { - masterEnabled.value = enabled - } - override val sendMessageEnabled = MutableStateFlow(true) - - override fun setSendMessageEnabled(enabled: Boolean) { - sendMessageEnabled.value = enabled - } - override val getMeshStatusEnabled = MutableStateFlow(true) - - override fun setGetMeshStatusEnabled(enabled: Boolean) { - getMeshStatusEnabled.value = enabled - } - override val getNodeListEnabled = MutableStateFlow(true) - - override fun setGetNodeListEnabled(enabled: Boolean) { - getNodeListEnabled.value = enabled - } - override val getChannelInfoEnabled = MutableStateFlow(true) - - override fun setGetChannelInfoEnabled(enabled: Boolean) { - getChannelInfoEnabled.value = enabled - } - override val getDeviceStatusEnabled = MutableStateFlow(true) - - override fun setGetDeviceStatusEnabled(enabled: Boolean) { - getDeviceStatusEnabled.value = enabled - } - override val getNodeDetailsEnabled = MutableStateFlow(true) - - override fun setGetNodeDetailsEnabled(enabled: Boolean) { - getNodeDetailsEnabled.value = enabled - } - override val getMeshMetricsEnabled = MutableStateFlow(true) - - override fun setGetMeshMetricsEnabled(enabled: Boolean) { - getMeshMetricsEnabled.value = enabled - } - override val getRecentMessagesEnabled = MutableStateFlow(true) - - override fun setGetRecentMessagesEnabled(enabled: Boolean) { - getRecentMessagesEnabled.value = enabled - } - override val getUnreadSummaryEnabled = MutableStateFlow(true) - override fun setGetUnreadSummaryEnabled(enabled: Boolean) { - getUnreadSummaryEnabled.value = enabled + override fun toggle(setting: AppFunctionsSetting) { + val flow = + when (setting) { + AppFunctionsSetting.MASTER -> masterEnabled + AppFunctionsSetting.SEND_MESSAGE -> sendMessageEnabled + AppFunctionsSetting.GET_MESH_STATUS -> getMeshStatusEnabled + AppFunctionsSetting.GET_NODE_LIST -> getNodeListEnabled + AppFunctionsSetting.GET_CHANNEL_INFO -> getChannelInfoEnabled + AppFunctionsSetting.GET_DEVICE_STATUS -> getDeviceStatusEnabled + AppFunctionsSetting.GET_NODE_DETAILS -> getNodeDetailsEnabled + AppFunctionsSetting.GET_MESH_METRICS -> getMeshMetricsEnabled + AppFunctionsSetting.GET_RECENT_MESSAGES -> getRecentMessagesEnabled + AppFunctionsSetting.GET_UNREAD_SUMMARY -> getUnreadSummaryEnabled + } + flow.update { !it } } } diff --git a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/Location.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeApplicationCoroutineScope.kt similarity index 60% rename from core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/Location.kt rename to core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeApplicationCoroutineScope.kt index 9c3e8ad6af..ce280d4ab1 100644 --- a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/Location.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeApplicationCoroutineScope.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.testing -import org.meshtastic.core.repository.Location +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import org.meshtastic.core.common.di.ApplicationCoroutineScope -/** Creates an Android [Location] for testing. */ -actual fun createLocation(latitude: Double, longitude: Double, altitude: Double): Location = Location("fake").apply { - this.latitude = latitude - this.longitude = longitude - this.altitude = altitude -} +/** An [ApplicationCoroutineScope] on [dispatcher], independent of any caller's scope, as the real one is. */ +class FakeApplicationCoroutineScope(dispatcher: CoroutineDispatcher) : + ApplicationCoroutineScope, + CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher) diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeBle.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeBle.kt index de90468c36..7eb4e50a2d 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeBle.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeBle.kt @@ -181,6 +181,7 @@ class FakeBleConnection : onDisconnect?.invoke() } + @Suppress("InjectDispatcher") // test double: Unconfined delivers notifications synchronously, see below override suspend fun profile( serviceUuid: Uuid, timeout: Duration, @@ -358,10 +359,14 @@ class FakeBluetoothRepository : /** Every device passed to [bond], in call order — lets tests assert that bonding was (or was not) attempted. */ val bondCalls = mutableListOf() + /** Set false to model hardware with no Bluetooth LE, such as an Android XR headset. */ + override var isSupported: Boolean = true + init { registerResetAction { bondOutcome = BondOutcome.Success bondCalls.clear() + isSupported = true } } diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt index 2b87584851..b772fcd60a 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.testing import kotlinx.coroutines.flow.Flow -import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.FirmwareReleaseRepository /** diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocationRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocationRepository.kt index daee1aee7e..3870a4471f 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocationRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocationRepository.kt @@ -40,6 +40,3 @@ class FakeLocationRepository : LocationRepository { _locations.emit(location) } } - -/** Platform-specific factory for creating [Location] objects in tests. */ -expect fun createLocation(latitude: Double, longitude: Double, altitude: Double = 0.0): Location diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt index ca699d5456..80424ebd3f 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.testing import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.MeshLog @@ -46,21 +47,32 @@ class FakeMeshLogRepository : var lastDeletedLocalStatsNodeNum: Int? = null private set + var deleteLogsOlderThanCalls = 0 + private set + + /** Runs at the start of every [deleteLogsOlderThan], so a test can hold a prune open or make it fail. */ + var beforeDeleteLogsOlderThan: suspend () -> Unit = {} + override fun reset() { super.reset() lastDeletedOlderThan = null deleteAllCalled = false lastDeletedLocalStatsNodeNum = null + deleteLogsOlderThanCalls = 0 + beforeDeleteLogsOlderThan = {} } override fun getAllLogs(maxItem: Int): Flow> = logsFlow.map { it.take(maxItem) } - override fun getAllLogsInReceiveOrder(maxItem: Int): Flow> = logsFlow.map { it.take(maxItem) } + override fun readAllLogsInReceiveOrder(): Flow = flow { + logsFlow.value.sortedBy { it.received_date }.forEach { emit(it) } + } override fun getAllLogsUnbounded(): Flow> = logsFlow - override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> = - logsFlow.map { it.filter { log -> log.fromNum == nodeNum && log.portNum == portNum } } + override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> = logsFlow.map { + it.filter { log -> log.fromNum == nodeNum && log.portNum == portNum } + } override fun getMeshPacketsFrom(nodeNum: Int, portNum: Int): Flow> = MutableStateFlow(emptyList()) @@ -97,6 +109,8 @@ class FakeMeshLogRepository : } override suspend fun deleteLogsOlderThan(retentionDays: Int) { + deleteLogsOlderThanCalls++ + beforeDeleteLogsOlderThan() lastDeletedOlderThan = retentionDays val window = MeshLogRetention.windowOrNull(retentionDays) ?: return val cutoff = nowMillis - window.inWholeMilliseconds diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshNotificationManager.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshNotificationManager.kt index c33d51273b..1209469abe 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshNotificationManager.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshNotificationManager.kt @@ -17,19 +17,42 @@ package org.meshtastic.core.testing import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.FirmwareUpdateNotice +import org.meshtastic.core.model.MeshBeaconOffer import org.meshtastic.core.model.Node import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.Notification import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry -/** A test double for [MeshNotificationManager] that provides a no-op implementation. */ -@Suppress("TooManyFunctions", "EmptyFunctionBlock") +/** Records every notification posted or cancelled. The `accepts*` flags stand in for a platform that declines. */ +@Suppress("TooManyFunctions") class FakeMeshNotificationManager : MeshNotificationManager { - override fun clearNotifications() {} + data class ClientPost(val notification: ClientNotification, val title: String, val severity: Notification.Type) - override fun initChannels() {} + var acceptsFirmwareUpdate = true + var acceptsReconnectBlocked = true - override fun updateServiceStateNotification(state: ConnectionState, telemetry: Telemetry?) {} + val meshBeacons = mutableListOf() + val newNodes = mutableListOf() + val cancelledNewNodes = mutableListOf() + val lowBatteryShown = mutableListOf() + val lowBatteryUpdated = mutableListOf() + val lowBatteryCancelled = mutableListOf() + val clientPosts = mutableListOf() + val clearedClientNotifications = mutableListOf() + val firmwareUpdateNotices = mutableListOf() + val reconnectBlocked = mutableListOf>() + var clearCount = 0 + private set + + override fun clearNotifications() { + clearCount++ + } + + override fun initChannels() = Unit + + override fun updateServiceStateNotification(state: ConnectionState, telemetry: Telemetry?) = Unit override suspend fun updateMessageNotification( contactKey: String, @@ -38,7 +61,7 @@ class FakeMeshNotificationManager : MeshNotificationManager { isBroadcast: Boolean, channelName: String?, isSilent: Boolean, - ) {} + ) = Unit override suspend fun updateWaypointNotification( contactKey: String, @@ -46,7 +69,7 @@ class FakeMeshNotificationManager : MeshNotificationManager { message: String, waypointId: Int, isSilent: Boolean, - ) {} + ) = Unit override suspend fun updateReactionNotification( contactKey: String, @@ -55,19 +78,55 @@ class FakeMeshNotificationManager : MeshNotificationManager { isBroadcast: Boolean, channelName: String?, isSilent: Boolean, - ) {} + ) = Unit - override fun showAlertNotification(contactKey: String, name: String, alert: String) {} + override suspend fun showAlertNotification(contactKey: String, name: String, alert: String) = Unit - override fun showNewNodeSeenNotification(node: Node) {} + override suspend fun showMeshBeaconNotification(offer: MeshBeaconOffer) { + meshBeacons += offer + } - override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {} + override suspend fun showNewNodeSeenNotification(node: Node, title: String) { + newNodes += node + } - override fun showClientNotification(clientNotification: ClientNotification) {} + override fun cancelNewNodeNotification(nodeNum: Int) { + cancelledNewNodes += nodeNum + } - override suspend fun cancelMessageNotification(contactKey: String) {} + override suspend fun showLowBatteryNotification(node: Node, isRemote: Boolean) { + lowBatteryShown += node + } - override fun cancelLowBatteryNotification(node: Node) {} + override suspend fun updateLowBatteryNotification(node: Node, isRemote: Boolean) { + lowBatteryUpdated += node + } - override fun clearClientNotification(notification: ClientNotification) {} + override fun cancelLowBatteryNotification(node: Node) { + lowBatteryCancelled += node + } + + override suspend fun showClientNotification( + clientNotification: ClientNotification, + title: String, + severity: Notification.Type, + ) { + clientPosts += ClientPost(clientNotification, title, severity) + } + + override fun clearClientNotification(clientNotification: ClientNotification) { + clearedClientNotifications += clientNotification + } + + override suspend fun showFirmwareUpdateNotification(notice: FirmwareUpdateNotice): Boolean { + if (acceptsFirmwareUpdate) firmwareUpdateNotices += notice + return acceptsFirmwareUpdate + } + + override suspend fun showReconnectBlockedNotification(title: String, message: String): Boolean { + if (acceptsReconnectBlocked) reconnectBlocked += title to message + return acceptsReconnectBlocked + } + + override suspend fun cancelMessageNotification(contactKey: String) = Unit } diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMessagingRepositories.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMessagingRepositories.kt index 54011d55d7..32d7a9f833 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMessagingRepositories.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMessagingRepositories.kt @@ -30,7 +30,7 @@ class FakePacketRepository { private val _packetsFlow = MutableStateFlow>(emptyList()) val packetsFlow: Flow> = _packetsFlow - suspend fun sendPacket(packet: DataPacket) { + fun sendPacket(packet: DataPacket) { sentPackets.add(packet) _packetsFlow.value = sentPackets.toList() } @@ -55,19 +55,19 @@ class FakeContactRepository { private val _contactsFlow = MutableStateFlow>(emptyList()) val contactsFlow: Flow> = _contactsFlow - suspend fun addContact(contact: Contact) { + fun addContact(contact: Contact) { contacts[contact.userId] = contact _contactsFlow.value = contacts.values.toList() } - suspend fun removeContact(userId: String) { + fun removeContact(userId: String) { contacts.remove(userId) _contactsFlow.value = contacts.values.toList() } - suspend fun getContact(userId: String): Contact? = contacts[userId] + fun getContact(userId: String): Contact? = contacts[userId] - suspend fun updateContactLastMessage(userId: String, time: Long) { + fun updateContactLastMessage(userId: String, time: Long) { contacts[userId]?.let { existing -> contacts[userId] = existing.copy(lastMessageTime = time) _contactsFlow.value = contacts.values.toList() diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt index 2155424850..b5efb64175 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.testing import kotlinx.coroutines.flow.Flow -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.repository.QuickChatActionRepository /** diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt index e27899e3c3..0089f6b333 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt @@ -115,6 +115,18 @@ class FakeRadioController : /** Every local or admin owner write, preserving destination and call order together. */ val ownerWrites = mutableListOf() + /** + * One admin request as it reached the controller: which request, for which node, carrying what, under which packet + * ID. [packetId] is null for requests whose controller method takes none. + */ + data class AdminRequest(val kind: String, val destNum: Int, val payload: Any?, val packetId: Int?) + + /** Every admin get/set request in call order, including those with no dedicated list above. */ + val adminRequests = mutableListOf() + + /** The value [generatePacketId] returns. */ + var nextPacketId: Int = 1 + /** Destination node for every edit transaction, in order. Local edits use the fake's sentinel value of zero. */ val editSettingsDestinations = mutableListOf() @@ -152,6 +164,12 @@ class FakeRadioController : /** Failure thrown by [requestNeighborInfo], when set. */ var requestNeighborInfoFailure: Exception? = null + + /** Failure thrown by [setFavorite], when set. */ + var setFavoriteFailure: Exception? = null + + /** Failure thrown by [sendSharedContact], when set. */ + var sendSharedContactFailure: Exception? = null val neighborInfoRequests = mutableListOf>() /** @@ -195,6 +213,8 @@ class FakeRadioController : fixedPositions.clear() moduleConfigWrites.clear() ownerWrites.clear() + adminRequests.clear() + nextPacketId = 1 editSettingsDestinations.clear() adminOperations.clear() failEditSettingsBegin = false @@ -208,6 +228,8 @@ class FakeRadioController : rejectLocalConfigWritesRemaining = 0 rejectLocalChannelWritesRemaining = 0 requestNeighborInfoFailure = null + setFavoriteFailure = null + sendSharedContactFailure = null neighborInfoRequests.clear() failChannelWriteAfter = null lastSetDeviceAddress = null @@ -234,10 +256,12 @@ class FakeRadioController : } override suspend fun setFavorite(nodeNum: Int, favorite: Boolean) { + setFavoriteFailure?.let { throw it } if (favorite) favoritedNodes.add(nodeNum) else favoritedNodes.remove(nodeNum) } override suspend fun sendSharedContact(nodeNum: Int): Boolean { + sendSharedContactFailure?.let { throw it } sentSharedContacts.add(nodeNum) return true } @@ -294,18 +318,27 @@ class FakeRadioController : lastSetOwnerUser = user ownerWrites.add(OwnerWrite(destination = destNum.takeUnless { it == 0 }, user = user)) adminOperations.add("owner") + recordAdminRequest("setOwner", destNum, user, packetId) } - override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) {} + override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) { + recordAdminRequest("setHamMode", destNum, hamParameters, packetId) + } override suspend fun setConfig(destNum: Int, config: Config, packetId: Int) { + recordAdminRequest("setConfig", destNum, config, packetId) recordConfigWrite(destNum, config, invokeStandaloneHook = true) } override suspend fun setModuleConfig(destNum: Int, config: ModuleConfig, packetId: Int) { + recordAdminRequest("setModuleConfig", destNum, config, packetId) recordModuleConfigWrite(destNum, config, invokeStandaloneHook = true) } + private fun recordAdminRequest(kind: String, destNum: Int, payload: Any?, packetId: Int?) { + adminRequests.add(AdminRequest(kind, destNum, payload, packetId)) + } + private suspend fun recordConfigWrite(destNum: Int, config: Config, invokeStandaloneHook: Boolean) { configWrites.add(ConfigWrite(destination = destNum.takeUnless { it == 0 }, config = config)) settingsOperations.add(SettingsOperation.SetConfig(config)) @@ -324,37 +357,57 @@ class FakeRadioController : channelWrites.add(ChannelWrite(destination = destNum.takeUnless { it == 0 }, channel = channel)) settingsOperations.add(SettingsOperation.SetChannel(channel)) adminOperations.add("channel:${channel.index}") + recordAdminRequest("setRemoteChannel", destNum, channel, packetId) } override suspend fun setFixedPosition(destNum: Int, position: Position) { + recordAdminRequest("setFixedPosition", destNum, position, packetId = null) onSetFixedPosition(destNum, position) fixedPositions.add(position) adminOperations.add("fixed-position") } - override suspend fun setRingtone(destNum: Int, ringtone: String) {} + override suspend fun setRingtone(destNum: Int, ringtone: String) { + recordAdminRequest("setRingtone", destNum, ringtone, packetId = null) + } - override suspend fun setCannedMessages(destNum: Int, messages: String) {} + override suspend fun setCannedMessages(destNum: Int, messages: String) { + recordAdminRequest("setCannedMessages", destNum, messages, packetId = null) + } override suspend fun setTime(destNum: Int, packetId: Int) {} - override suspend fun getOwner(destNum: Int, packetId: Int) {} + override suspend fun getOwner(destNum: Int, packetId: Int) { + recordAdminRequest("getOwner", destNum, payload = null, packetId) + } - override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) {} + override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) { + recordAdminRequest("getConfig", destNum, configType, packetId) + } - override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) {} + override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) { + recordAdminRequest("getModuleConfig", destNum, moduleConfigType, packetId) + } - override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) {} + override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) { + recordAdminRequest("getChannel", destNum, index, packetId) + } - override suspend fun getRingtone(destNum: Int, packetId: Int) {} + override suspend fun getRingtone(destNum: Int, packetId: Int) { + recordAdminRequest("getRingtone", destNum, payload = null, packetId) + } - override suspend fun getCannedMessages(destNum: Int, packetId: Int) {} + override suspend fun getCannedMessages(destNum: Int, packetId: Int) { + recordAdminRequest("getCannedMessages", destNum, payload = null, packetId) + } - override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) {} + override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) { + recordAdminRequest("getDeviceConnectionStatus", destNum, payload = null, packetId) + } override suspend fun reboot(destNum: Int, packetId: Int) {} - override suspend fun rebootToDfu(nodeNum: Int) {} + override suspend fun rebootToDfu(nodeNum: Int, packetId: Int) {} override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { onRequestRebootOta(requestId, destNum, mode, hash) @@ -397,6 +450,7 @@ class FakeRadioController : return current === expected } + @Suppress("SuspendFunSwallowedCancellation") // mirrors production: the failure is held until the commit override suspend fun editSettings(destNum: Int, block: suspend AdminEditScope.() -> Unit) { editSettingsDestinations.add(destNum) editSettingsCalled = true @@ -443,7 +497,7 @@ class FakeRadioController : override suspend fun editLocalSettings(block: suspend AdminEditScope.() -> Unit) = editSettings(0, block) - override fun generatePacketId(): Int = 1 + override fun generatePacketId(): Int = nextPacketId override fun startProvideLocation() { startProvideLocationCalled = true diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceService.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceService.kt index 26dd7fbffd..ff3caa6d11 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceService.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceService.kt @@ -115,8 +115,11 @@ class FakeRadioInterfaceService(override val serviceScope: CoroutineScope = Main } } - override suspend fun runWhileSessionActive(session: RadioSessionContext, block: suspend () -> Unit): Boolean = - sessionOperationMutex.withLock { runWithSessionLease(session) { block() } } + override suspend fun runWhileSessionActive( + session: RadioSessionContext, + label: String, + block: suspend () -> Unit, + ): Boolean = sessionOperationMutex.withLock { runWithSessionLease(session) { block() } } // Use an unbounded Channel to mirror SharedRadioInterfaceService semantics. A MutableSharedFlow would // hide the stop/start backlog bug that motivated the resetReceivedBuffer() API. diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt index b57f443e53..4f07f6f156 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt @@ -53,6 +53,7 @@ private val SETTLE_POLL_INTERVAL = 5.milliseconds * `runTest` advances the virtual clock to the next scheduled task whenever that happens, firing pending `delay`s early. * Such a test should instead load the resources it needs before scheduling anything it later advances past. */ +@Suppress("InjectDispatcher") // waits in real time, off the test scheduler, by design suspend fun TestScope.runUntilSettled(timeout: Duration = 10.seconds, isSettled: () -> Boolean) { val start = TimeSource.Monotonic.markNow() while (true) { diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt index aa5f1e002f..a3cebc0a12 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt @@ -126,6 +126,6 @@ class FakeBleServiceFailureInjectionTest { assertFalse(subscribed, "onSubscription must not run for never-subscribe characteristic") assertNotNull(received, "Notifications must still flow through the bare SharedFlow") - assertTrue(received!!.contentEquals(byteArrayOf(1, 2, 3)), "Notification payload must be exposed verbatim") + assertTrue(received.contentEquals(byteArrayOf(1, 2, 3)), "Notification payload must be exposed verbatim") } } diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt index d1ab367737..a91309dbfc 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt @@ -21,13 +21,13 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.QuickChatAction import org.meshtastic.core.model.ConnectionEpochs import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.repository.EditSettingsTransactionException import org.meshtastic.core.repository.LocalNodeUnavailableException import org.meshtastic.core.repository.PacketQueueRejectedException diff --git a/core/ui/README.md b/core/ui/README.md index 576a82e635..bb62bd1c61 100644 --- a/core/ui/README.md +++ b/core/ui/README.md @@ -52,15 +52,10 @@ MeshtasticResourceDialog( graph TB :core:ui[ui]:::kmp-library-compose :core:ui -.-> :core:common - :core:ui -.-> :core:data - :core:ui -.-> :core:database - :core:ui -.-> :core:datastore :core:ui -.-> :core:model :core:ui -.-> :core:navigation - :core:ui -.-> :core:prefs :core:ui -.-> :core:repository :core:ui -.-> :core:resources - :core:ui -.-> :core:service :core:ui -.-> :core:testing classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 979eb3291d..069d98002e 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -24,6 +24,8 @@ plugins { } kotlin { + // No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT. + // Required for CMP files/ resources (emoji-data.json) to be packaged as Android assets. // Without this, Res.readBytes() throws MissingResourceException at runtime. android { androidResources.enable = true } @@ -31,16 +33,11 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) implementation(projects.core.model) implementation(projects.core.navigation) - implementation(projects.core.prefs) implementation(libs.meshtastic.protobufs) implementation(projects.core.repository) implementation(projects.core.resources) - implementation(projects.core.service) implementation(libs.compose.multiplatform.animation) implementation(libs.compose.multiplatform.material3) @@ -73,8 +70,6 @@ kotlin { implementation(libs.jetbrains.lifecycle.runtime.compose) } - getByName("jvmAndroidMain") { dependencies { implementation(libs.compose.multiplatform.ui.tooling) } } - androidMain.dependencies { implementation(libs.androidx.activity.compose) } commonTest.dependencies { diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml index cacd9cf0b6..8ce27952e1 100644 --- a/core/ui/detekt-baseline.xml +++ b/core/ui/detekt-baseline.xml @@ -12,7 +12,6 @@ ComposableParamOrder:MainAppBar.kt:@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun MainAppBar ComposableParamOrder:MaterialBatteryInfo.kt:@Suppress("MagicNumber", "LongMethod") @Composable fun MaterialBatteryInfo ComposableParamOrder:NodeChip.kt:@Composable fun NodeChip - ComposableParamOrder:NodeKeyStatusIcon.kt:@Composable fun NodeKeyStatusIcon ComposableParamOrder:SignalInfo.kt:@Composable fun SignalInfo ComposableParamOrder:SwitchPreference.kt:@Composable fun SwitchPreference CompositionLocalAllowlist:LocalAnalyticsIntroProvider.kt:val LocalAnalyticsIntroProvider = compositionLocalOf<@Composable () -> Unit> { {} } @@ -23,24 +22,20 @@ CompositionLocalAllowlist:LocalNfcScannerProvider.kt:val LocalNfcScannerProvider = compositionLocalOf<@Composable (onResult: (String?) -> Unit, onNfcDisabled: () -> Unit) -> Unit> { { _, _ -> } } CompositionLocalAllowlist:LocalNfcScannerProvider.kt:val LocalNfcScannerSupported = compositionLocalOf { false } CompositionLocalAllowlist:LocalNfcScannerProvider.kt:val LocalNfcWriterProvider = compositionLocalOf<@Composable (url: String, onResult: (Boolean) -> Unit, onNfcDisabled: () -> Unit) -> Unit> { { _, _, _ -> } } - CompositionLocalAllowlist:LocalNodeMapScreenProvider.kt:/** * Provides the platform-specific Map Screen for a Node (e.g. Google Maps or OSMDroid on Android). On Desktop or JVM * targets where native maps aren't available yet, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeMapScreenProvider = compositionLocalOf<@Composable (destNum: Int, onNavigateUp: () -> Unit) -> Unit> { { destNum, _ -> PlaceholderScreen("Node Map ($destNum)") } } - CompositionLocalAllowlist:LocalNodeTrackMapProvider.kt:/** * Provides an embeddable position-track map composable that renders a polyline with markers for the given [positions]. * Unlike [LocalNodeMapScreenProvider], this does **not** include a Scaffold or AppBar — it is designed to be embedded * inside another screen layout (e.g. the position-log adaptive layout). * * Supports optional synchronized selection: * - [selectedPositionTime]: the `Position.time` of the currently selected position (or `null` for no selection). When * non-null, the map should visually highlight the corresponding marker and center the camera on it. * - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so * the host can synchronize the card list. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeTrackMapProvider = compositionLocalOf< @Composable ( destNum: Int, positions: List<Position>, modifier: Modifier, selectedPositionTime: Int?, onPositionSelected: ((Int) -> Unit)?, ) -> Unit, > { { _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } } + CompositionLocalAllowlist:LocalNodeTrackMapProvider.kt:/** * Provides an embeddable position-track map composable that renders a polyline with markers for the given [positions]. * It has no Scaffold or AppBar, so it can be embedded inside another screen layout (e.g. the position-log adaptive * layout). * * Supports optional synchronized selection: * - [selectedPositionTime]: the `Position.time` of the currently selected position (or `null` for no selection). When * non-null, the map should visually highlight the corresponding marker and center the camera on it. * - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so * the host can synchronize the card list. * - [showAttribution]: whether the credit opens with the map or stays collapsed behind its own button. The embedded * instance collapses it; a full-screen one does not. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeTrackMapProvider = compositionLocalOf< @Composable ( destNum: Int, positions: List<Position>, modifier: Modifier, selectedPositionTime: Int?, onPositionSelected: ((Int) -> Unit)?, showAttribution: Boolean, ) -> Unit, > { { _, _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } } CompositionLocalAllowlist:LocalTracerouteMapOverlayInsetsProvider.kt:val LocalTracerouteMapOverlayInsetsProvider = compositionLocalOf { TracerouteMapOverlayInsets() } CompositionLocalAllowlist:LocalTracerouteMapProvider.kt:/** * Provides an embeddable traceroute map composable that renders node markers and forward/return offset polylines for a * traceroute result. Unlike [LocalMapViewProvider], this does **not** include a Scaffold, AppBar, waypoints, location * tracking, custom tiles, or any main-map features — it is designed to be embedded inside `TracerouteMapScreen`'s * scaffold. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. * * Parameters: * - `tracerouteOverlay`: The overlay with forward/return route node nums. * - `tracerouteNodePositions`: Map of node num to position snapshots for the route nodes. * - `onMappableCountChanged`: Callback with (shown, total) node counts. * - `modifier`: Compose modifier for the map. */ @Suppress("Wrapping") val LocalTracerouteMapProvider = compositionLocalOf< @Composable ( tracerouteOverlay: TracerouteOverlay?, tracerouteNodePositions: Map<Int, Position>, onMappableCountChanged: (Int, Int) -> Unit, modifier: Modifier, ) -> Unit, > { { _, _, _, _ -> PlaceholderScreen("Traceroute Map") } } - CompositionLocalAllowlist:LocalTracerouteMapScreenProvider.kt:/** * Provides the platform-specific Traceroute Map Screen. On Desktop or JVM targets where native maps aren't available * yet, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalTracerouteMapScreenProvider = compositionLocalOf<@Composable (destNum: Int, requestId: Int, logUuid: String?, onNavigateUp: () -> Unit) -> Unit> { { _, _, _, _ -> PlaceholderScreen("Traceroute Map") } } CompositionLocalAllowlist:MapViewProvider.kt:val LocalMapViewProvider = compositionLocalOf<MapViewProvider?> { null } FunctionTypeModifierSpacing:Theme.kt:@Composable() - LambdaParameterInRestartableEffect:EmojiPickerDialog.kt:onCategoryChanged: (Int) -> Unit LambdaParameterInRestartableEffect:PlatformUtils.kt:check: () -> Boolean - LambdaParameterInRestartableEffect:TracerouteAlertHandler.kt:onNavigateToMap: (destinationNodeNum: Int, requestId: Int, logUuid: String?) -> Unit + LongParameterList:ConnectionsViewModel.kt:ConnectionsViewModel MagicNumber:EditIPv4Preference.kt:0xff MagicNumber:EditIPv4Preference.kt:16 MagicNumber:EditIPv4Preference.kt:24 MagicNumber:EditIPv4Preference.kt:8 - MagicNumber:EditListPreference.kt:12345 - MagicNumber:EditListPreference.kt:67890 - MagicNumber:LazyColumnDragAndDropDemo.kt:50 MatchingDeclarationName:LocalTracerouteMapOverlayInsetsProvider.kt:TracerouteMapOverlayInsets + MissingNonRestartableComposable:AlertDialogs.kt:@Composable fun MeshtasticResourceDialog + MissingNonRestartableComposable:AlertDialogs.kt:@Composable fun MeshtasticTextDialog ModifierMissing:ChannelItem.kt:@Composable fun ChannelItem ModifierMissing:ChannelSelection.kt:@Composable fun ChannelSelection ModifierMissing:IndoorAirQuality.kt:@Suppress("LongMethod", "UnusedPrivateProperty") @Composable fun IndoorAirQuality @@ -58,6 +53,10 @@ ModifierReused:TextDividerPreference.kt:Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } } MultipleEmitters:PreferenceCategory.kt:@Composable fun PreferenceCategory MutableStateAutoboxing:EmojiPickerDialog.kt:mutableStateOf(0) + NoNameShadowing:PositionPrecisionPreference.kt:enabled + NoNameShadowing:ScannedQrCodeDialog.kt:wb + NoNameShadowing:SharedContactImportDialog.kt:wb + NoNameShadowing:SwitchPreference.kt:loading ParameterNaming:BitwisePreference.kt:onItemSelected: (Int) -> Unit ParameterNaming:ChannelSelection.kt:onSelected: (Boolean) -> Unit ParameterNaming:DropDownPreference.kt:onItemSelected: (T) -> Unit @@ -118,6 +117,18 @@ PreviewPublic:TextDividerPreference.kt:@Preview(showBackground = true) @Composable fun TextDividerPreferencePreview PreviewPublic:TitledCard.kt:@PreviewLightDark @Composable fun TitledCardPreview TooManyFunctions:NoopStubs.kt:org.meshtastic.core.ui.util.NoopStubs.kt + UnnecessaryLaunchedEffect:EditBase64Preference.kt:LaunchedEffect + UnnecessaryLaunchedEffect:FirmwareVersionCheck.kt:LaunchedEffect + UnnecessaryLaunchedEffect:MeshtasticNavDisplay.kt:LaunchedEffect + UnnecessaryLaunchedEffect:MeshtasticSearchBar.kt:LaunchedEffect + UnnecessaryLaunchedEffect:TracerouteAlertHandler.kt:LaunchedEffect + UnusedPrivateProperty:ConnectionsViewModel.kt:ConnectionsViewModel$private val deviceHardwareRepository: DeviceHardwareRepository + UnusedPrivateProperty:ConnectionsViewModel.kt:ConnectionsViewModel$private val firmwareReleaseRepository: FirmwareReleaseRepository + UnusedPrivateProperty:ConnectionsViewModel.kt:ConnectionsViewModel$private val radioPrefs: RadioPrefs + UnusedPrivateProperty:UIViewModel.kt:UIViewModel$private val eventFirmwareRepository: EventFirmwareRepository + UseOrEmpty:AlertDialogs.kt:title ?: titleRes?.let { stringResource(it) } ?: "" + UseOrEmpty:ConnectionsViewModel.kt:ConnectionsViewModel$firmwareReleaseRepository.getManifestTargets(it.stableRelease) ?: emptySet() + UseOrEmpty:DropDownPreference.kt:currentItem?.label ?: "" ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.Nodes, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, ) ViewModelForwarding:MeshtasticCommonAppSetup.kt:FirmwareVersionCheck(viewModel = uiViewModel) ViewModelForwarding:MeshtasticCommonAppSetup.kt:SharedDialogs(uiViewModel = uiViewModel) diff --git a/feature/discovery/src/iosMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.ios.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt similarity index 71% rename from feature/discovery/src/iosMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.ios.kt rename to core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt index 5f9777a7af..9f0f1cf5c5 100644 --- a/feature/discovery/src/iosMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.ios.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt @@ -14,12 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.discovery.export +package org.meshtastic.core.ui.component +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable -import co.touchlab.kermit.Logger +import androidx.compose.ui.Modifier @Composable -actual fun rememberExportSaver(): ExportSaverLauncher = ExportSaverLauncher { result -> - Logger.w { "Export save not yet implemented on iOS: ${result.fileName}" } -} +@Suppress("UNUSED_PARAMETER") +actual fun BoxScope.ListScrollbar(state: LazyListState, modifier: Modifier) = Unit diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt index 63ec959caf..efd0b77511 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt @@ -23,10 +23,12 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +@Suppress("MissingReadOnlyComposable") // kept in step with the plain @Composable expect and the other actuals @Composable -actual fun dynamicColorScheme(darkTheme: Boolean): ColorScheme? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) -} else { - null -} +actual fun dynamicColorScheme(darkTheme: Boolean): ColorScheme? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + null + } diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/ContextExtensions.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/ContextExtensions.kt index 8994bb088d..b72813e7f4 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/ContextExtensions.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/ContextExtensions.kt @@ -27,7 +27,7 @@ suspend fun Context.showToast(stringResource: StringResource, vararg formatArgs: Toast.makeText(this, getString(stringResource, *formatArgs), Toast.LENGTH_SHORT).show() } -suspend fun Context.showToast(text: String) { +fun Context.showToast(text: String) { Toast.makeText(this, text, Toast.LENGTH_SHORT).show() } diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index d013ad19bc..90d571bddf 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -50,14 +50,12 @@ import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import co.touchlab.kermit.Logger -import com.eygraber.uri.toAndroidUri import com.eygraber.uri.toKmpUri -import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString import org.meshtastic.core.common.gpsDisabled +import org.meshtastic.core.common.hasBluetoothLe import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.common.util.ioDispatcher import java.net.URLEncoder @Composable @@ -186,32 +184,6 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT return remember(launcher) { { mimeType -> launcher.launch(mimeType) } } } -@Suppress("Wrapping") -@Composable -actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? { - val context = LocalContext.current - return remember(context) { - { uri, maxChars -> - withContext(ioDispatcher) { - @Suppress("TooGenericExceptionCaught") - try { - val androidUri = uri.toAndroidUri() - context.contentResolver.openInputStream(androidUri)?.use { stream -> - stream.bufferedReader().use { reader -> - val buffer = CharArray(maxChars) - val read = reader.read(buffer) - if (read > 0) String(buffer, 0, read) else null - } - } - } catch (e: Exception) { - Logger.e(e) { "Failed to read text from URI: $uri" } - null - } - } - } - } -} - @Composable actual fun KeepScreenOn(enabled: Boolean) { val view = LocalView.current @@ -291,6 +263,12 @@ actual fun rememberOpenWifiSettings(): () -> Unit { actual val bleScanRequiresLocationServices: Boolean = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S +@Composable +actual fun isBluetoothSupported(): Boolean { + val context = LocalContext.current + return remember(context) { context.hasBluetoothLe() } +} + @Composable actual fun isBluetoothDisabled(): Boolean { val context = LocalContext.current @@ -371,16 +349,15 @@ actual fun isWifiUnavailable(): Boolean { // until a callback-based rewrite is warranted. @Suppress("DEPRECATION") private fun ConnectivityManager.hasLocalNetwork(): Boolean { - val transports = - allNetworks.mapNotNull { network -> - getNetworkCapabilities(network)?.let { caps -> - NetworkTransportInfo( - hasWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI), - hasEthernet = caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET), - hasVpn = caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN), - ) - } + val transports = allNetworks.mapNotNull { network -> + getNetworkCapabilities(network)?.let { caps -> + NetworkTransportInfo( + hasWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI), + hasEthernet = caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET), + hasVpn = caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN), + ) } + } return anyNetworkScanTransportAvailable(transports) } @@ -426,6 +403,18 @@ actual fun rememberLocationPermissionState(): PermissionUiState = rememberRuntim requireAll = false, ) +@Composable +actual fun rememberPreciseLocationPermissionState(): PermissionUiState = rememberRuntimePermissionState( + // Android 12+ ignores a fine request that does not also ask for coarse. Fine leads so the rationale and the + // requested flag follow the permission that decides the grant. + permissions = + arrayOf( + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + ), + requireAll = true, +) + @Composable actual fun rememberBluetoothPermissionState(): PermissionUiState { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPane.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPane.kt index 5e07adc27d..0c828b3237 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPane.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPane.kt @@ -40,8 +40,10 @@ import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @@ -58,8 +60,8 @@ import org.meshtastic.core.ui.theme.AppTheme * ([MeshtasticNavDisplay]) use, so both surfaces flip to two panes at the same breakpoint. * * When split, the panes are hosted in a [SupportingPaneScaffold] so the divider is a draggable [VerticalDragHandle], - * giving parity with the list-detail / supporting-pane scenes elsewhere in the app. Both slots keep their [ColumnScope] - * receiver, so callers are unchanged. + * giving parity with the list-detail scenes elsewhere in the app. Both slots keep their [ColumnScope] receiver, so + * callers are unchanged. * * The scaffold reports its incoming max height as its own size, so a height-unbounded host (a LazyColumn item, a * scrollable column) would make it echo Constraints.Infinity and crash; a plain [Row] split is used there instead. @@ -76,8 +78,12 @@ fun AdaptiveTwoPane( // Wrap the slots in movable content so their internal state survives when the layout flips between the stacked // column and the two-pane scaffold (e.g. on resize / fold), and so neither slot is emitted directly from two // branches. The ColumnScope is passed through, so the compact branch keeps the shared-column behaviour. - val firstPane = remember { movableContentOf(first) } - val secondPane = remember { movableContentOf(second) } + // The movable content is created once, so it calls the slots through state to follow a caller that passes a + // different lambda instance. + val currentFirst by rememberUpdatedState(first) + val currentSecond by rememberUpdatedState(second) + val firstPane = remember { movableContentOf { currentFirst(it) } } + val secondPane = remember { movableContentOf { currentSecond(it) } } // Hoisted above the height branch so a dragged divider survives the host flipping between bounded and // unbounded constraints (the scaffold branch below leaves composition on that flip). diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt index e59230ec3a..bd49414e4c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt @@ -44,6 +44,7 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.okay +import org.meshtastic.core.ui.theme.link import org.meshtastic.core.ui.util.annotatedStringFromHtml /** @@ -103,7 +104,7 @@ fun MeshtasticDialog( SpanStyle( textDecoration = TextDecoration.Underline, fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.link, ), ), ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AutoLinkText.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AutoLinkText.kt index 33d45e91ed..8aa39151c7 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AutoLinkText.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AutoLinkText.kt @@ -37,10 +37,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import org.meshtastic.core.model.MENTION_TOKEN_REGEX -import org.meshtastic.core.ui.theme.HyperlinkBlue +import org.meshtastic.core.ui.theme.link -private val DefaultTextLinkStyles = - TextLinkStyles(style = SpanStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline)) +@Composable +private fun defaultTextLinkStyles(): TextLinkStyles { + val link = MaterialTheme.colorScheme.link + return remember(link) { TextLinkStyles(style = SpanStyle(color = link, textDecoration = TextDecoration.Underline)) } +} private val WEB_URL_REGEX = Regex( @@ -57,8 +60,6 @@ private val EMAIL_REGEX = private val PHONE_REGEX = Regex("""(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}""") -private val MentionSpanStyle = SpanStyle(color = HyperlinkBlue, fontWeight = FontWeight.Bold) - /** * A [Text] component that automatically detects and linkifies URLs, email addresses, and phone numbers. * @@ -71,7 +72,7 @@ fun AutoLinkText( text: String, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, - linkStyles: TextLinkStyles = DefaultTextLinkStyles, + linkStyles: TextLinkStyles = defaultTextLinkStyles(), color: Color = Color.Unspecified, textAlign: TextAlign? = null, mentionName: ((String) -> String?)? = null, @@ -144,12 +145,14 @@ internal fun buildAnnotatedStringWithLinks( append(display) val usedIndices = mutableSetOf() + val mentionStyle = + TextLinkStyles( + SpanStyle(color = linkStyles.style?.color ?: Color.Unspecified, fontWeight = FontWeight.Bold), + ) for ((range, id) in substitution.mentions) { addLink( - LinkAnnotation.Clickable(tag = "mention", styles = TextLinkStyles(MentionSpanStyle)) { - onMentionClick(id) - }, + LinkAnnotation.Clickable(tag = "mention", styles = mentionStyle) { onMentionClick(id) }, range.first, range.last + 1, ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BuildNodeDescription.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BuildNodeDescription.kt index eacf391962..c4c78730dc 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BuildNodeDescription.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BuildNodeDescription.kt @@ -18,13 +18,15 @@ package org.meshtastic.core.ui.component import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.a11y_node_battery import org.meshtastic.core.resources.a11y_node_distance_away import org.meshtastic.core.resources.a11y_node_favorite -import org.meshtastic.core.resources.a11y_node_hops_away +import org.meshtastic.core.resources.a11y_node_hops_count import org.meshtastic.core.resources.a11y_node_last_heard import org.meshtastic.core.resources.a11y_node_offline import org.meshtastic.core.resources.a11y_node_online @@ -36,7 +38,6 @@ import org.meshtastic.core.resources.unknown import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.proto.Config.LoRaConfig.ModemPreset -private const val MILLIS_PER_SECOND = 1000L private const val MAX_BATTERY_PERCENT = 100 /** Pre-resolved localized strings for TalkBack node descriptions. */ @@ -56,15 +57,15 @@ internal data class NodeDescriptionStrings( val incomplete: String, ) -/** Resolves [NodeDescriptionStrings] from Compose string resources. */ +/** Resolves [NodeDescriptionStrings] from Compose string resources, with the hop count's plural for [hopsAway]. */ @Composable -internal fun rememberNodeDescriptionStrings(): NodeDescriptionStrings = NodeDescriptionStrings( +internal fun rememberNodeDescriptionStrings(hopsAway: Int): NodeDescriptionStrings = NodeDescriptionStrings( online = stringResource(Res.string.a11y_node_online), offline = stringResource(Res.string.a11y_node_offline), favorite = stringResource(Res.string.a11y_node_favorite), lastHeard = stringResource(Res.string.a11y_node_last_heard, "%s"), role = stringResource(Res.string.a11y_node_role, "%s"), - hopsAway = stringResource(Res.string.a11y_node_hops_away, 0), + hopsAway = pluralStringResource(Res.plurals.a11y_node_hops_count, hopsAway, hopsAway), battery = stringResource(Res.string.a11y_node_battery, 0), distanceAway = stringResource(Res.string.a11y_node_distance_away, "%s"), signal = stringResource(Res.string.a11y_node_signal, "%s"), @@ -107,7 +108,7 @@ internal fun buildNodeDescription( if (lastHeardIsRelative) { formatAgo(lastHeard, strings.unknown, strings.now) } else { - DateFormatter.formatDateTime(lastHeard.toLong() * MILLIS_PER_SECOND) + DateFormatter.formatDateTime(lastHeard.toLong() * TimeConstants.MS_PER_SEC) } append(", ") append(strings.lastHeard.replace("%s", timeText)) @@ -116,7 +117,7 @@ internal fun buildNodeDescription( append(strings.role.replace("%s", role)) if (hopsAway > 0) { append(", ") - append(strings.hopsAway.replace("0", hopsAway.toString())) + append(strings.hopsAway) } batteryLevel?.let { if (it in 1..MAX_BATTERY_PERCENT) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ConnectionsNavIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ConnectionsNavIcon.kt index 5aed67880e..03a86029cf 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ConnectionsNavIcon.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ConnectionsNavIcon.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Size @@ -74,6 +75,7 @@ fun ConnectionsNavIcon( } @Composable +@ReadOnlyComposable private fun getTint(connectionState: ConnectionState): Color = when (connectionState) { ConnectionState.Connecting -> colorScheme.StatusOrange ConnectionState.Disconnected -> colorScheme.StatusRed diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt index 1a8e8ca2bf..43c736396e 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt @@ -26,7 +26,6 @@ import org.meshtastic.core.model.util.toSharedContact import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.share_contact import org.meshtastic.core.resources.share_contact_subject -import org.meshtastic.proto.SharedContact /** * Displays a dialog with the contact's information as a QR code and URI. @@ -39,7 +38,7 @@ import org.meshtastic.proto.SharedContact * @param onDismiss Callback invoked when the dialog is dismissed. */ @Composable -fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit, isOwnContact: Boolean = false) { +fun ShareContactDialog(contact: Node?, onDismiss: () -> Unit, isOwnContact: Boolean = false) { if (contact == null) return val contactToShare = contact.toSharedContact(isOwnContact) val uriString = contactToShare.getSharedContactUrl().toString() @@ -47,18 +46,7 @@ fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit, isOwnContact: Boo title = stringResource(Res.string.share_contact), uriString = uriString, onDismiss = onDismiss, - subtitle = contact.user?.long_name, + subtitle = contact.user.long_name, shareSubject = stringResource(Res.string.share_contact_subject), ) } - -/** - * Displays a dialog for importing a shared contact. - * - * @param sharedContact The [SharedContact] to import. - * @param onDismiss Callback invoked when the dialog is dismissed. - */ -@Composable -fun SharedContactImportDialog(sharedContact: SharedContact, onDismiss: () -> Unit) { - org.meshtastic.core.ui.share.SharedContactDialog(sharedContact = sharedContact, onDismiss = onDismiss) -} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt index b518cdd77b..754c66863a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt @@ -44,6 +44,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.schemaLabelRes import org.meshtastic.core.ui.theme.AppTheme import kotlin.jvm.JvmName @@ -59,14 +61,18 @@ fun > DropDownPreference( itemColor: @Composable ((T) -> Color)? = null, itemLabel: @Composable ((T) -> String)? = null, ) { + // A deprecated value the radio currently holds stays on the list: dropping it leaves the field showing nothing and + // writes a different value on the next save. val enumConstants = remember(selectedItem) { - enumEntriesOf(selectedItem).filter { it.name != "UNRECOGNIZED" && !it.isDeprecatedEnumEntry() } + enumEntriesOf(selectedItem).filter { + it.name != "UNRECOGNIZED" && (it == selectedItem || !it.isDeprecatedEnumEntry()) + } } val items = enumConstants.map { - val label = itemLabel?.invoke(it) ?: it.name + val label = itemLabel?.invoke(it) ?: it.schemaLabel() val icon = itemIcon?.invoke(it) val color = itemColor?.invoke(it) DropDownItem(it, label, icon, color) @@ -218,6 +224,9 @@ internal expect fun > enumEntriesOf(selectedItem: T): List internal expect fun Enum<*>.isDeprecatedEnumEntry(): Boolean +/** The label the schema gives this value, falling back to the constant's name where the schema does not name it. */ +@Composable private fun Enum<*>.schemaLabel(): String = schemaLabelRes()?.let { stringResource(it) } ?: name + @Preview(showBackground = true) @Composable fun DropDownPreferencePreview() { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyDetailPlaceholder.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyDetailPlaceholder.kt deleted file mode 100644 index ac66c425a9..0000000000 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyDetailPlaceholder.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.ui.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp - -/** - * Generic empty-state placeholder for detail panes in list-detail layouts. - * - * Shows a centered icon and title, styled with [MaterialTheme.colorScheme.onSurfaceVariant]. Used by both nodes and - * conversations adaptive screens on Android and Desktop. - */ -@Composable -fun EmptyDetailPlaceholder(icon: ImageVector, title: String, modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } -} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EmptyStateContent.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyState.kt similarity index 56% rename from feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EmptyStateContent.kt rename to core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyState.kt index 9d4986bf3c..e645311c95 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EmptyStateContent.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EmptyState.kt @@ -14,11 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.ui.components +package org.meshtastic.core.ui.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon @@ -31,33 +32,48 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +/** + * Centered empty state: a muted icon, a title saying what is empty, an optional line saying what happens next, and an + * optional action. Text uses `onSurface` and `onSurfaceVariant`, which meet 4.5:1 on every surface; `outline` does not. + */ @Composable -fun EmptyStateContent( - text: String, - imageVector: ImageVector, +fun EmptyState( + icon: ImageVector, + title: String, modifier: Modifier = Modifier, + supportingText: String? = null, action: (@Composable () -> Unit)? = null, ) { Column( - modifier = modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.Center, + modifier = modifier.padding(horizontal = 32.dp, vertical = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { Icon( - imageVector = imageVector, + imageVector = icon, contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) + Spacer(modifier = Modifier.height(12.dp)) Text( - text = text, - modifier = Modifier.padding(top = 16.dp), - style = MaterialTheme.typography.bodyLarge, + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.outline, ) + if (supportingText != null) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = supportingText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } if (action != null) { - Column(modifier = Modifier.padding(top = 24.dp)) { action() } + Spacer(modifier = Modifier.height(16.dp)) + action() } } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt index e1d0b0f6c9..51b18c6256 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt @@ -55,6 +55,7 @@ import org.meshtastic.core.ui.icon.Nfc import org.meshtastic.core.ui.icon.Person import org.meshtastic.core.ui.icon.QrCode2 import org.meshtastic.core.ui.icon.QrCodeScanner +import org.meshtastic.core.ui.share.SharedContactImportDialog import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider import org.meshtastic.core.ui.util.LocalBarcodeScannerSupported @@ -98,8 +99,9 @@ fun MeshtasticImportFAB( var isNfcScanning by rememberSaveable { mutableStateOf(false) } var showNfcDisabledDialog by rememberSaveable { mutableStateOf(false) } - val barcodeScanner = - LocalBarcodeScannerProvider.current { contents -> normalizeImportContents(contents)?.let(onImport) } + val barcodeScanner = LocalBarcodeScannerProvider.current { contents -> + normalizeImportContents(contents)?.let(onImport) + } val nfcScanner = LocalNfcScannerProvider.current val isNfcSupported = LocalNfcScannerSupported.current val isBarcodeSupported = LocalBarcodeScannerSupported.current diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt index 1150ead035..52c5b100b5 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt @@ -53,6 +53,7 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.air_quality_icon import org.meshtastic.core.resources.close +import org.meshtastic.core.resources.iaq_value import org.meshtastic.core.resources.indoor_air_quality_iaq import org.meshtastic.core.resources.preview_dot import org.meshtastic.core.resources.preview_gauge @@ -140,7 +141,11 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil modifier = Modifier.padding(4.dp).align(Alignment.CenterStart), verticalAlignment = Alignment.CenterVertically, ) { - Text(text = "IAQ $iaq", color = Color.White, fontWeight = FontWeight.Bold) + Text( + text = stringResource(Res.string.iaq_value, iaq), + color = Color.White, + fontWeight = FontWeight.Bold, + ) Icon( imageVector = if (iaqEnum.range.first < 100) MeshtasticIcons.ThumbUp else MeshtasticIcons.Warning, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt index 6666c6fca2..89dbed2def 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt @@ -25,14 +25,13 @@ import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowSeconds +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ic_antenna import org.meshtastic.core.resources.node_sort_last_heard import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.formatAgo -private const val MILLIS_PER_SECOND = 1000L - @Composable fun LastHeardInfo( modifier: Modifier = Modifier, @@ -45,7 +44,7 @@ fun LastHeardInfo( if (relative) { formatAgo(lastHeard) } else { - DateFormatter.formatDateTime(lastHeard.toLong() * MILLIS_PER_SECOND) + DateFormatter.formatDateTime(lastHeard.toLong() * TimeConstants.MS_PER_SEC) } IconInfo( modifier = modifier, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt index cb4b398d0c..29f3379836 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt @@ -159,7 +159,6 @@ fun BasicListItem( } } -@Composable fun ImageVector?.icon(tint: Color = Color.Unspecified): @Composable (() -> Unit)? = this?.let { { val resolvedTint = if (tint == Color.Unspecified) LocalContentColor.current else tint diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt new file mode 100644 index 0000000000..6b6c86ba73 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** Draggable vertical scrollbar pinned to the end edge of the enclosing [BoxScope]; drawn on desktop only. */ +@Composable expect fun BoxScope.ListScrollbar(state: LazyListState, modifier: Modifier = Modifier) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt index be885686fc..7147ef1f1e 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.external_power_short import org.meshtastic.core.resources.unknown import org.meshtastic.core.ui.icon.BatteryEmpty import org.meshtastic.core.ui.icon.BatteryUnknown @@ -83,7 +84,7 @@ fun MaterialBatteryInfo( ) Text( - text = "PWR", + text = stringResource(Res.string.external_power_short), color = contentColor.copy(alpha = 0.95f), style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold, fontSize = 12.sp), ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt index 07bc14575f..0954f45ae1 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt @@ -31,19 +31,20 @@ import androidx.compose.material3.adaptive.layout.PaneExpansionState import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy -import androidx.compose.material3.adaptive.navigation3.rememberSupportingPaneSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavEntryDecorator import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.scene.DialogSceneStrategy import androidx.navigation3.scene.Scene import androidx.navigation3.ui.NavDisplay import co.touchlab.kermit.Logger @@ -55,7 +56,10 @@ import org.meshtastic.core.repository.PlatformAnalytics * Shared [NavDisplay] wrapper that configures the standard Meshtastic entry decorators, scene strategies, and * transition animations for all platform hosts. * - * This version supports multiple backstacks by accepting a [MultiBackstack] state holder. + * This version supports multiple backstacks by accepting a [MultiBackstack] state holder. Every tab's stack is + * decorated with that tab's own saveable-state and ViewModel-store decorators and only the active tab's entries reach + * [NavDisplay], so showing another tab clears nothing while an entry removed from any stack is still cleared. + * [entryProvider] must resolve the keys of every tab, not only the active one. */ @Composable fun MeshtasticNavDisplay( @@ -64,19 +68,28 @@ fun MeshtasticNavDisplay( modifier: Modifier = Modifier, analytics: PlatformAnalytics? = null, ) { - val backStack = multiBackstack.activeBackStack - MeshtasticNavDisplay( - backStack = backStack, + val activeTab = multiBackstack.currentTabRoute + MeshtasticNavDisplayHost( + backStack = multiBackstack.activeBackStack, onBack = { multiBackstack.goBack() }, - entryProvider = entryProvider, modifier = modifier, analytics = analytics, - ) + ) { + val entriesByTab = + multiBackstack.backStacks.mapValues { (tab, stack) -> + key(tab) { + val keys = stack.toList() + val isActive = tab == activeTab + // Host providers close over the active stack, so a tab's entries are rebuilt when it activates. + val entries = remember(keys, isActive) { keys.map(entryProvider) } + rememberDecoratedNavEntries(entries, rememberMeshtasticEntryDecorators()) + } + } + entriesByTab.getValue(activeTab) + } } /** Shared [NavDisplay] wrapper for a single backstack. */ -@Suppress("LongMethod") -@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class) @Composable fun MeshtasticNavDisplay( backStack: NavBackStack, @@ -84,6 +97,39 @@ fun MeshtasticNavDisplay( modifier: Modifier = Modifier, onBack: (() -> Unit)? = null, analytics: PlatformAnalytics? = null, +) { + val decorators = rememberMeshtasticEntryDecorators() + MeshtasticNavDisplayHost( + backStack = backStack, + onBack = + onBack + ?: { + if (backStack.size > 1) { + backStack.removeLastOrNull() + } + }, + modifier = modifier, + analytics = analytics, + ) { + rememberDecoratedNavEntries(backStack, decorators, entryProvider) + } +} + +@Composable +private fun rememberMeshtasticEntryDecorators(): List> { + val saveableDecorator = rememberSaveableStateHolderNavEntryDecorator() + val vmStoreDecorator = rememberViewModelStoreNavEntryDecorator() + return remember(saveableDecorator, vmStoreDecorator) { listOf(saveableDecorator, vmStoreDecorator) } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class) +@Composable +private fun MeshtasticNavDisplayHost( + backStack: NavBackStack, + onBack: () -> Unit, + analytics: PlatformAnalytics?, + modifier: Modifier = Modifier, + decorateEntries: @Composable () -> List>, ) { // Root captured at first composition; a stale entry back handler can drain the stack mid-transition // and NavDisplay rejects an empty backstack (fatal in the field), so self-heal back to the root. @@ -103,40 +149,25 @@ fun MeshtasticNavDisplay( DisposableEffect(tracker) { onDispose { tracker.dispose() } } } + val entries = decorateEntries() + val listDetailSceneStrategy = rememberListDetailSceneStrategy( paneExpansionState = rememberPaneExpansionState(), paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, ) - val supportingPaneSceneStrategy = - rememberSupportingPaneSceneStrategy( - paneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, - ) - - val saveableDecorator = rememberSaveableStateHolderNavEntryDecorator() - val vmStoreDecorator = rememberViewModelStoreNavEntryDecorator() - - val activeDecorators = - remember(backStack, saveableDecorator, vmStoreDecorator) { listOf(saveableDecorator, vmStoreDecorator) } // Fades are alpha, not movement, so they follow the theme's effects spec rather than a spatial one. val fadeSpec = MaterialTheme.motionScheme.defaultEffectsSpec() + // No screen declares shared elements; NavDisplay itself uses this scope to animate an entry that moves between + // scenes, such as a detail going from a single pane to the list-detail split on resize. SharedTransitionLayout { NavDisplay( - backStack = backStack, - entryProvider = entryProvider, - entryDecorators = activeDecorators, - onBack = - onBack - ?: { - if (backStack.size > 1) { - backStack.removeLastOrNull() - } - }, - // NavDisplay falls back to SinglePaneSceneStrategy automatically when none of these compute a Scene. - sceneStrategies = listOf(DialogSceneStrategy(), listDetailSceneStrategy, supportingPaneSceneStrategy), + entries = entries, + onBack = onBack, + // NavDisplay falls back to SinglePaneSceneStrategy automatically when this computes no Scene. + sceneStrategies = listOf(listDetailSceneStrategy), sharedTransitionScope = this@SharedTransitionLayout, transitionSpec = meshtasticTransitionSpec(fadeSpec), popTransitionSpec = meshtasticTransitionSpec(fadeSpec), @@ -146,7 +177,7 @@ fun MeshtasticNavDisplay( } } -/** Drag handle shared by the list-detail and supporting-pane scene strategies, with a 48.dp touch-target floor. */ +/** Drag handle for the list-detail scene strategy, with a 48.dp touch-target floor. */ @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable private fun ThreePaneScaffoldScope.PaneExpansionDragHandle(state: PaneExpansionState) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSearchBar.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSearchBar.kt new file mode 100644 index 0000000000..a8464a7d44 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSearchBar.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.placeCursorAtEnd +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.ExpandedDockedSearchBar +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SearchBarScrollBehavior +import androidx.compose.material3.SearchBarState +import androidx.compose.material3.SearchBarValue +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.rememberSearchBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.clear +import org.meshtastic.core.resources.navigate_back +import org.meshtastic.core.ui.icon.ArrowBack +import org.meshtastic.core.ui.icon.Close +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Search + +/** Appended to the caller's tag for the field the expanded overlay shows, so a test can target one of the two. */ +const val SEARCH_BAR_EXPANDED_TAG_SUFFIX = "Expanded" + +/** + * The Material 3 search bar every Meshtastic search field is built from: a collapsed [SearchBar] that expands into a + * results surface over the screen. + * + * The expanded half follows Material's own rule rather than the caller's: full-screen on compact width, docked on + * anything wider, because a full-screen dialog over a tablet or desktop window hides context the user was reading. + * + * @param query the text the field starts with, so a query that survived process death restores into the bar. Later + * changes to it are ignored; the field is the source of truth once it exists. See [resetKey]. + * @param onQueryChange called for every edit made through the field. + * @param placeholder shown in the empty field, and the field's accessibility name. + * @param resetKey change this to make the field take [query] again, for a caller that clears the search from outside. + * @param inputFieldTag test tag for the collapsed field; the expanded one takes it plus + * [SEARCH_BAR_EXPANDED_TAG_SUFFIX]. + * @param scrollBehavior lets the bar react to the content scrolling under it; see + * [SearchBarDefaults.enterAlwaysSearchBarScrollBehavior]. + * @param trailingActions extra icons after the clear button, for controls that belong to the search itself (a sort + * menu, a filter menu). They render in the collapsed and expanded field alike. + * @param expandedContent the results shown under the field once expanded. + */ +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun MeshtasticSearchBar( + query: String, + onQueryChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + clearDescription: String = stringResource(Res.string.clear), + resetKey: Any? = Unit, + inputFieldTag: String = SEARCH_BAR_INPUT_FIELD_TAG, + scrollBehavior: SearchBarScrollBehavior? = null, + trailingActions: @Composable RowScope.() -> Unit = {}, + expandedContent: @Composable ColumnScope.() -> Unit = {}, +) { + // The field owns the text; [query] only seeds it, and [resetKey] is how a caller replaces it. + // + // Feeding [query] back in on every change would lose keystrokes wherever it makes a round trip that lags the + // typing - the node list's does, through SavedStateHandle and a combined flow - because a stale value arriving + // while the field is further ahead overwrites what was typed. + val textFieldState = rememberTextFieldState(query) + val searchBarState = rememberSearchBarState() + val latestOnQueryChange by rememberUpdatedState(onQueryChange) + val latestQuery by rememberUpdatedState(query) + + LaunchedEffect(resetKey) { + if (textFieldState.text.toString() != latestQuery) { + textFieldState.edit { + replace(0, length, latestQuery) + placeCursorAtEnd() + } + } + } + // Edits made through the field are reported back to the caller. + LaunchedEffect(textFieldState) { + snapshotFlow { textFieldState.text.toString() }.collect { latestOnQueryChange(it) } + } + + val inputField: @Composable (String) -> Unit = { tag -> + SearchInputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + placeholder = placeholder, + clearDescription = clearDescription, + testTag = tag, + trailingActions = trailingActions, + ) + } + + val barModifier = + modifier.fillMaxWidth().let { base -> + scrollBehavior?.let { behavior -> with(behavior) { base.searchBarScrollBehavior() } } ?: base + } + SearchBar(state = searchBarState, inputField = { inputField(inputFieldTag) }, modifier = barModifier) + + val expandedTag = inputFieldTag + SEARCH_BAR_EXPANDED_TAG_SUFFIX + // Material docks the expanded bar on medium and larger windows and only goes full-screen on compact ones. The + // pane directive is the width test the rest of the app already adapts on, so search agrees with the panes. + val splitsHorizontally = calculatePaneScaffoldDirective(currentWindowAdaptiveInfoV2()).maxHorizontalPartitions > 1 + if (splitsHorizontally) { + ExpandedDockedSearchBar( + state = searchBarState, + inputField = { inputField(expandedTag) }, + content = expandedContent, + ) + } else { + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = { inputField(expandedTag) }, + content = expandedContent, + ) + } +} + +/** Default tag for the collapsed field, for callers with only one search bar on screen. */ +const val SEARCH_BAR_INPUT_FIELD_TAG = "SearchBarInputField" + +/** The [SearchBarDefaults.InputField] shared by the collapsed bar and the expanded overlay. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchInputField( + textFieldState: TextFieldState, + searchBarState: SearchBarState, + placeholder: String, + clearDescription: String, + testTag: String, + trailingActions: @Composable RowScope.() -> Unit, +) { + val scope = rememberCoroutineScope() + val isExpanded = searchBarState.currentValue == SearchBarValue.Expanded + val backDescription = stringResource(Res.string.navigate_back) + + SearchBarDefaults.InputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + onSearch = { scope.launch { searchBarState.animateToCollapsed() } }, + placeholder = { Text(placeholder) }, + leadingIcon = { + if (isExpanded) { + IconButton(onClick = { scope.launch { searchBarState.animateToCollapsed() } }) { + Icon(imageVector = MeshtasticIcons.ArrowBack, contentDescription = backDescription) + } + } else { + Icon(imageVector = MeshtasticIcons.Search, contentDescription = null) + } + }, + trailingIcon = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (textFieldState.text.isNotEmpty()) { + IconButton(onClick = { textFieldState.clearText() }) { + Icon(imageVector = MeshtasticIcons.Close, contentDescription = clearDescription) + } + } + trailingActions() + } + }, + modifier = Modifier.testTag(testTag).semantics { contentDescription = placeholder }, + ) +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt index 4ceefd78fb..eab4fb9ee6 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt @@ -124,18 +124,12 @@ fun NodeItem( FontStyle.Normal } - val unmessageable = - remember(thatNode) { - when { - thatNode.user.is_unmessagable != null -> thatNode.user.is_unmessagable!! - else -> thatNode.user.role.isUnmessageableRole() - } - } + val unmessageable = remember(thatNode) { thatNode.user.is_unmessagable ?: thatNode.user.role.isUnmessageableRole() } // Resolved out here, not inside the remember: stringResource is composable, and the description is built in a // plain lambda. val roleLabel = stringResource(thatNode.user.role.label) - val a11yStrings = rememberNodeDescriptionStrings() + val a11yStrings = rememberNodeDescriptionStrings(hopsAway = thatNode.hopsAway) val modemPreset = LocalModemPreset.current val nodeDescription = remember(thatNode, distance, a11yStrings, modemPreset, roleLabel) { @@ -284,7 +278,8 @@ private fun NodeBatteryPositionRow( } } -@Suppress("CyclomaticComplexMethod", "LongMethod") +// signalChip is assigned once while the list is built, in the same composition that reads it. +@Suppress("CyclomaticComplexMethod", "LongMethod", "VarsWithoutStateBacking") @Composable private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Color) { // The signal pill bundles SNR + RSSI + quality into one row. It's wider than a 1/3 grid cell, so it renders on @@ -355,7 +350,6 @@ private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Col } @Suppress("LongMethod", "CyclomaticComplexMethod") -@Composable private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: Color): List<@Composable () -> Unit> { val items = mutableListOf<@Composable () -> Unit>() val env = node.environmentMetrics diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt index dae2125624..e63a1882d6 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import org.meshtastic.core.common.util.MeasurementSystem @@ -59,11 +60,16 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.isUnmessageableRole import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.a11y_humidity +import org.meshtastic.core.resources.a11y_node_channel +import org.meshtastic.core.resources.a11y_node_hops_count +import org.meshtastic.core.resources.a11y_temperature import org.meshtastic.core.resources.distance import org.meshtastic.core.resources.ic_memory import org.meshtastic.core.resources.node_incomplete import org.meshtastic.core.resources.node_list_click_label import org.meshtastic.core.resources.node_list_long_click_label +import org.meshtastic.core.resources.pressure import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.ui.icon.Channel import org.meshtastic.core.ui.icon.Counter0 @@ -125,13 +131,7 @@ fun NodeItemCompact( thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) } val bearingDegrees = remember(thisNode, thatNode) { thisNode?.bearing(thatNode) } - val unmessageable = - remember(thatNode) { - when { - thatNode.user.is_unmessagable != null -> thatNode.user.is_unmessagable!! - else -> thatNode.user.role.isUnmessageableRole() - } - } + val unmessageable = remember(thatNode) { thatNode.user.is_unmessagable ?: thatNode.user.role.isUnmessageableRole() } val contentColor = MaterialTheme.colorScheme.onSurface val nodeColor = @@ -144,7 +144,7 @@ fun NodeItemCompact( // Resolved out here, not inside the remember: stringResource is composable, and the description is built in a // plain lambda. val roleLabel = stringResource(thatNode.user.role.label) - val a11yStrings = rememberNodeDescriptionStrings() + val a11yStrings = rememberNodeDescriptionStrings(hopsAway = thatNode.hopsAway) val modemPreset = LocalModemPreset.current val nodeDescription = remember(thatNode, distance, lastHeardIsRelative, a11yStrings, modemPreset) { @@ -425,7 +425,12 @@ private fun CompactFooterRow( add { IconInfo( icon = MeshtasticIcons.HopCount, - contentDescription = "${thatNode.hopsAway} hops", + contentDescription = + pluralStringResource( + Res.plurals.a11y_node_hops_count, + thatNode.hopsAway, + thatNode.hopsAway, + ), contentColor = tertiaryColor, text = thatNode.hopsAway.toString(), ) @@ -435,7 +440,7 @@ private fun CompactFooterRow( add { Icon( imageVector = channelIcon(thatNode.channel), - contentDescription = "Channel ${thatNode.channel}", + contentDescription = stringResource(Res.string.a11y_node_channel, thatNode.channel), modifier = Modifier.size(COMPACT_ICON_SIZE_DP.dp), tint = tertiaryColor, ) @@ -469,7 +474,7 @@ private fun CompactMetricsRow(thatNode: Node, tempInFahrenheit: Boolean, content add { IconInfo( icon = MeshtasticIcons.Temperature, - contentDescription = "Temperature", + contentDescription = stringResource(Res.string.a11y_temperature), contentColor = contentColor, text = temp, ) @@ -479,7 +484,7 @@ private fun CompactMetricsRow(thatNode: Node, tempInFahrenheit: Boolean, content add { IconInfo( icon = MeshtasticIcons.Humidity, - contentDescription = "Humidity", + contentDescription = stringResource(Res.string.a11y_humidity), contentColor = contentColor, text = MetricFormatter.humidity(env.relative_humidity ?: 0f), ) @@ -489,7 +494,7 @@ private fun CompactMetricsRow(thatNode: Node, tempInFahrenheit: Boolean, content add { IconInfo( icon = MeshtasticIcons.Pressure, - contentDescription = "Pressure", + contentDescription = stringResource(Res.string.pressure), contentColor = contentColor, text = MetricFormatter.pressure(env.barometric_pressure ?: 0f), ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt index 77967b720e..52244ca9d0 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState @@ -193,7 +194,7 @@ fun QrDialog( // Copy stays an icon: it is the secondary of the three, and a tooltip carries the // label on desktop, where standards section 4 asks for one on an icon-only control. TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(stringResource(Res.string.copy)) } }, state = rememberTooltipState(), ) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SharedDialogs.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SharedDialogs.kt index effc8c54f9..c6047881c2 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SharedDialogs.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SharedDialogs.kt @@ -21,11 +21,11 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.ui.qr.ScannedQrCodeDialog -import org.meshtastic.core.ui.share.SharedContactDialog +import org.meshtastic.core.ui.share.SharedContactImportDialog import org.meshtastic.core.ui.viewmodel.UIViewModel /** - * Shared composable that conditionally renders [SharedContactDialog] and [ScannedQrCodeDialog] when the device is + * Shared composable that conditionally renders [SharedContactImportDialog] and [ScannedQrCodeDialog] when the device is * connected and requests are pending. * * This eliminates identical boilerplate from Android `MainScreen` and Desktop `DesktopMainScreen`. @@ -38,7 +38,7 @@ fun SharedDialogs(uiViewModel: UIViewModel) { if (connectionState == ConnectionState.Connected) { sharedContactRequested?.let { - SharedContactDialog(sharedContact = it, onDismiss = { uiViewModel.clearSharedContactRequested() }) + SharedContactImportDialog(sharedContact = it, onDismiss = { uiViewModel.clearSharedContactRequested() }) } requestChannelSet?.let { newChannelSet -> diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt index cfb1022bac..f0acc09cb9 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -58,6 +59,7 @@ fun TracerouteAlertHandler( var dismissedTracerouteRequestId by remember { mutableStateOf(null) } val colorScheme = MaterialTheme.colorScheme val scope = rememberCoroutineScope() + val currentOnNavigateToMap by rememberUpdatedState(onNavigateToMap) LaunchedEffect(traceRouteResponse, dismissedTracerouteRequestId) { val response = traceRouteResponse @@ -89,7 +91,7 @@ fun TracerouteAlertHandler( val errorRes = availability.toMessageRes() if (errorRes == null) { dismissedTracerouteRequestId = response.requestId - onNavigateToMap(response.destinationNodeNum, response.requestId, response.logUuid) + currentOnNavigateToMap(response.destinationNodeNum, response.requestId, response.logUuid) } else { uiViewModel.clearTracerouteResponse() // Post the error alert after the current alert is dismissed to avoid diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt index aa35275673..78dc293dc0 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt @@ -62,11 +62,11 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow @@ -84,6 +84,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -150,12 +151,12 @@ fun EmojiPickerDialog( onConfirm: (String) -> Unit, ) { val viewModel: EmojiPickerViewModel = koinViewModel() - val isLoaded by viewModel.isLoaded.collectAsState() - val loadError by viewModel.loadError.collectAsState() + val isLoaded by viewModel.isLoaded.collectAsStateWithLifecycle() + val loadError by viewModel.loadError.collectAsStateWithLifecycle() var searchQuery by rememberSaveable { mutableStateOf("") } var debouncedQuery by remember { mutableStateOf("") } var selectedCategoryIndex by rememberSaveable { mutableStateOf(0) } - val preferredSkinToneIndex by viewModel.preferredSkinToneIndex.collectAsState() + val preferredSkinToneIndex by viewModel.preferredSkinToneIndex.collectAsStateWithLifecycle() // Debounce search input to avoid per-keystroke filtering of 1870 emojis LaunchedEffect(searchQuery) { @@ -415,7 +416,9 @@ private fun EmojiGrid( } // Sync tab selection with scroll position - LaunchedEffect(gridState, searchQuery) { + val currentSelectedCategoryIndex by rememberUpdatedState(selectedCategoryIndex) + val currentOnCategoryChanged by rememberUpdatedState(onCategoryChanged) + LaunchedEffect(gridState, searchQuery, gridItems, tabOffset) { if (searchQuery.isNotBlank()) return@LaunchedEffect snapshotFlow { gridState.firstVisibleItemIndex } .collect { firstVisible -> @@ -428,10 +431,10 @@ private fun EmojiGrid( 0 } else { val catIdx = item.key.removePrefix(CATEGORY_HEADER_KEY_PREFIX).toIntOrNull() - if (catIdx != null) catIdx + tabOffset else selectedCategoryIndex + if (catIdx != null) catIdx + tabOffset else currentSelectedCategoryIndex } - if (newIndex != selectedCategoryIndex) { - onCategoryChanged(newIndex) + if (newIndex != currentSelectedCategoryIndex) { + currentOnCategoryChanged(newIndex) } break } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt index a4259f6fb3..19954067c2 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.ui.emoji import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -54,6 +55,8 @@ internal class EmojiPickerViewModel( } catch (e: MissingResourceException) { Logger.e(tag = "EmojiPickerViewModel", throwable = e) { "Failed to load emoji data" } _loadError.value = true + } catch (e: CancellationException) { + throw e } catch (e: IllegalStateException) { Logger.e(tag = "EmojiPickerViewModel", throwable = e) { "Failed to load emoji data" } _loadError.value = true diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/icon/Hardware.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/icon/Hardware.kt index 1f04a6687e..fb9634e63d 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/icon/Hardware.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/icon/Hardware.kt @@ -23,6 +23,7 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ic_bluetooth import org.meshtastic.core.resources.ic_bluetooth_connected import org.meshtastic.core.resources.ic_bluetooth_searching +import org.meshtastic.core.resources.ic_build import org.meshtastic.core.resources.ic_cached import org.meshtastic.core.resources.ic_display_settings import org.meshtastic.core.resources.ic_memory @@ -35,6 +36,9 @@ import org.meshtastic.core.resources.ic_usb import org.meshtastic.core.resources.ic_usb_off import org.meshtastic.core.resources.ic_wifi +/** The maker hardware mark from meshtastic/design#160: a wrench, Material Symbols "build". */ +val MeshtasticIcons.Wrench: ImageVector + @Composable get() = vectorResource(Res.drawable.ic_build) val MeshtasticIcons.BluetoothConnected: ImageVector @Composable get() = vectorResource(Res.drawable.ic_bluetooth_connected) val MeshtasticIcons.BluetoothSearching: ImageVector diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt index 15d27b1a5c..d8161705b8 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt @@ -49,17 +49,28 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.schemaLabelRes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.accept import org.meshtastic.core.resources.add import org.meshtastic.core.resources.add_channels_description import org.meshtastic.core.resources.cancel +import org.meshtastic.core.resources.disabled +import org.meshtastic.core.resources.enabled +import org.meshtastic.core.resources.lora_config_change_value +import org.meshtastic.core.resources.lora_config_changes import org.meshtastic.core.resources.new_channel_rcvd import org.meshtastic.core.resources.replace import org.meshtastic.core.resources.replace_channels_and_settings_description +import org.meshtastic.core.resources.schema_lora_hop_limit +import org.meshtastic.core.resources.schema_lora_modem_preset +import org.meshtastic.core.resources.schema_lora_region +import org.meshtastic.core.resources.schema_lora_use_preset +import org.meshtastic.core.resources.unknown import org.meshtastic.core.ui.component.ChannelSelection import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.getChannelPreviewForAdd @@ -181,26 +192,26 @@ fun ScannedQrCodeDialog( if (shouldReplace && incoming.lora_config != null) { val current = channels.lora_config val new = incoming.lora_config - val changes = mutableListOf() - - if (current?.hop_limit != new?.hop_limit) { - changes.add("Hop Limit: ${current?.hop_limit} -> ${new?.hop_limit}") + buildList { + if (current?.hop_limit != new?.hop_limit) { + add(LoraConfigChange(Res.string.schema_lora_hop_limit, current?.hop_limit, new?.hop_limit)) + } + if (current?.region != new?.region) { + add(LoraConfigChange(Res.string.schema_lora_region, current?.region, new?.region)) + } + if (current?.modem_preset != new?.modem_preset) { + add( + LoraConfigChange( + Res.string.schema_lora_modem_preset, + current?.modem_preset, + new?.modem_preset, + ), + ) + } + if (current?.use_preset != new?.use_preset) { + add(LoraConfigChange(Res.string.schema_lora_use_preset, current?.use_preset, new?.use_preset)) + } } - if (current?.region != new?.region) { - val currentRegionDesc = current?.region?.name ?: "Unknown" - val newRegionDesc = new?.region?.name ?: "Unknown" - changes.add("Region: $currentRegionDesc -> $newRegionDesc") - } - if (current?.modem_preset != new?.modem_preset) { - val currentPresetDesc = current?.modem_preset?.name ?: "Unknown" - val newPresetDesc = new?.modem_preset?.name ?: "Unknown" - changes.add("Modem Preset: $currentPresetDesc -> $newPresetDesc") - } - if (current?.use_preset != new?.use_preset) { - changes.add("Use Preset: ${current?.use_preset} -> ${new?.use_preset}") - } - - changes } else { emptyList() } @@ -269,13 +280,20 @@ fun ScannedQrCodeDialog( if (shouldReplace && loraChanges.isNotEmpty()) { item { Text( - text = "LoRa Configuration Changes:", + text = stringResource(Res.string.lora_config_changes), modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), style = MaterialTheme.typography.titleMedium, ) loraChanges.forEach { change -> + val line = + stringResource( + Res.string.lora_config_change_value, + stringResource(change.label), + loraConfigValueText(change.from), + loraConfigValueText(change.to), + ) Text( - text = "• $change", + text = "• $line", modifier = Modifier.padding(start = 16.dp, bottom = 4.dp), style = MaterialTheme.typography.bodyMedium, ) @@ -355,6 +373,16 @@ fun ScannedQrCodeDialog( } } +private data class LoraConfigChange(val label: StringResource, val from: Any?, val to: Any?) + +@Composable +private fun loraConfigValueText(value: Any?): String = when (value) { + null -> stringResource(Res.string.unknown) + is Boolean -> stringResource(if (value) Res.string.enabled else Res.string.disabled) + is Enum<*> -> value.schemaLabelRes()?.let { stringResource(it) } ?: value.name + else -> value.toString() +} + @PreviewLightDark @Composable private fun ScannedQrCodeDialogPreview() { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactImportDialog.kt similarity index 95% rename from core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt rename to core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactImportDialog.kt index c015ebdf8a..9f0eaecf14 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactImportDialog.kt @@ -53,7 +53,7 @@ import org.meshtastic.proto.User /** A dialog for importing a shared contact that was scanned from a QR code. */ @Composable -fun SharedContactDialog( +fun SharedContactImportDialog( sharedContact: SharedContact, onDismiss: () -> Unit, viewModel: SharedContactViewModel = koinViewModel(), @@ -62,7 +62,7 @@ fun SharedContactDialog( val node = unfilteredNodes.find { it.num == sharedContact.node_num } - SharedContactDialogContent( + SharedContactImportDialogContent( sharedContact = sharedContact, node = node, onDismiss = onDismiss, @@ -74,11 +74,11 @@ fun SharedContactDialog( } /** - * Stateless content of [SharedContactDialog]. [node] is the matching node already in the local database, or null when - * the contact is unknown. + * Stateless content of [SharedContactImportDialog]. [node] is the matching node already in the local database, or null + * when the contact is unknown. */ @Composable -fun SharedContactDialogContent( +fun SharedContactImportDialogContent( sharedContact: SharedContact, node: Node?, onDismiss: () -> Unit, @@ -140,7 +140,7 @@ private val PREVIEW_PUBLIC_KEY = ByteArray(32) { 0x2B.toByte() }.toByteString() fun PreviewSharedContactImportAlert() { AppTheme { Box(modifier = Modifier.fillMaxSize()) { - SharedContactDialogContent( + SharedContactImportDialogContent( sharedContact = SharedContact.Builder() .also { wb -> diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt index a528a0e20c..2311e64b70 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt @@ -19,7 +19,9 @@ package org.meshtastic.core.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance // ─── Brand Colors (Design Standards v1.3) ─── val MeshtasticGreen = Color(0xFF67EA94) // Green 500 — Brand Accent @@ -111,7 +113,17 @@ object SemanticColors { val SuccessLight = Color(0xFFE5FCEE) // Green 100 } -val HyperlinkBlue = Color(0xFF5C6BC0) // Blue 600 (Info) +/** + * The Link colour: Blue 800 on a light surface and Blue 300 on a dark one, the tones nearest the standards' Blue 600 + * and Blue 400 that hold 4.5:1 on every surface a link sits on here, node-tinted message cards included. Follows the + * active scheme's surface rather than the system, so a theme picked in the app is respected. + */ +val ColorScheme.link: Color + get() = if (surface.luminance() < DARK_SURFACE_LUMINANCE) BluePalette.B300 else BluePalette.B800 + +/** Midpoint luminance separating a light surface from a dark one; both static schemes sit near the extremes. */ +private const val DARK_SURFACE_LUMINANCE = 0.5f + val AnnotationColor = Color(0xFF2855A8) // Blue 700 (Accent) object TracerouteColors { @@ -168,6 +180,7 @@ object GraphColors { object StatusColors { val ColorScheme.StatusGreen: Color @Composable + @ReadOnlyComposable get() = if (isSystemInDarkTheme()) { Color(0xFF3FB86D) // Green 600 @@ -177,6 +190,7 @@ object StatusColors { val ColorScheme.StatusYellow: Color @Composable + @ReadOnlyComposable get() = if (isSystemInDarkTheme()) { Color(0xFFE8A33E) // Warning @@ -186,6 +200,7 @@ object StatusColors { val ColorScheme.StatusOrange: Color @Composable + @ReadOnlyComposable get() = if (isSystemInDarkTheme()) { Color(0xFFE07000) @@ -195,6 +210,7 @@ object StatusColors { val ColorScheme.StatusRed: Color @Composable + @ReadOnlyComposable get() = if (isSystemInDarkTheme()) { Color(0xFFE05252) // Error @@ -204,12 +220,20 @@ object StatusColors { val ColorScheme.StatusBlue: Color @Composable + @ReadOnlyComposable get() = if (isSystemInDarkTheme()) { Color(0xFF5C6BC0) // Info } else { Color(0xFF5C6BC0) // Info } + + /** + * The maker hardware rung's hue from meshtastic/design#160, shared with the flasher; declared, never derived. The + * one status colour whose light and dark values differ, so it follows the active scheme's surface, not the system. + */ + val ColorScheme.StatusSky: Color + get() = if (surface.luminance() < DARK_SURFACE_LUMINANCE) Color(0xFF7DD3FC) else Color(0xFF075985) } @Suppress("MagicNumber") diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt index 40e3d44d12..a94694cc14 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFontFamilyResolver import co.touchlab.kermit.Logger -import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.CancellationException private val lightScheme = lightColorScheme( diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.kt index 34efae175d..047f074176 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.kt @@ -32,7 +32,8 @@ data class DiscoveryMapNode( val shortName: String?, val longName: String?, val neighborType: DiscoveryNeighborType, - val snr: Float = 0f, + /** Null when no packet from this node reported an snr. */ + val snr: Float? = null, /** Null when no packet from this node reported an rssi. */ val rssi: Int? = null, val messageCount: Int = 0, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/FileExporter.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/FileExporter.kt new file mode 100644 index 0000000000..b47fc05548 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/FileExporter.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import co.touchlab.kermit.Logger +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.compose.koinInject +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.common.util.ioDispatcher +import org.meshtastic.core.common.util.safeCatching +import org.meshtastic.core.repository.FileService + +/** + * Returns a launcher that asks the user where to save a file, then writes the bytes [content] produces there through + * [FileService] and reports whether the export landed to [onResult]. [content] runs only after a destination is chosen; + * returning null means there is nothing to export. + */ +@Composable +fun rememberFileExporter( + content: suspend () -> ByteArray?, + onResult: suspend (exported: Boolean) -> Unit = {}, +): (fileName: String, mimeType: String) -> Unit { + val fileService: FileService = koinInject() + val scope = rememberCoroutineScope() + val currentContent by rememberUpdatedState(content) + val currentOnResult by rememberUpdatedState(onResult) + return rememberSaveFileLauncher { uri -> + scope.launch { currentOnResult(writeExport(fileService, uri, currentContent)) } + } +} + +/** + * Writes the bytes [content] produces to [uri]. Returns false without writing when [content] returns null or throws, + * and false when [FileService] reports the write failed. + */ +internal suspend fun writeExport(fileService: FileService, uri: CommonUri, content: suspend () -> ByteArray?): Boolean { + val bytes = + safeCatching { withContext(ioDispatcher) { content() } } + .onFailure { e -> Logger.e(e) { "Could not produce the export" } } + .getOrNull() ?: return false + return fileService.write(uri) { it.write(bytes) } +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt index 019630641d..00ddf9c3fc 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.layout.ContentScale import coil3.compose.AsyncImage import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone +import kotlinx.datetime.parseOrNull import kotlinx.datetime.todayIn import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @@ -132,7 +133,7 @@ fun EventBrandingIcon( * treated as ended. */ fun EventFirmwareEdition.hasEnded(): Boolean { - val end = eventEnd?.let { runCatching { LocalDate.parse(it) }.getOrNull() } ?: return false + val end = eventEnd?.let { LocalDate.parseOrNull(it) } ?: return false val zone = timeZone?.let { runCatching { TimeZone.of(it) }.getOrNull() } ?: TimeZone.currentSystemDefault() return Clock.System.todayIn(zone) > end } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeTrackMapProvider.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeTrackMapProvider.kt index d0901f0f9d..6f1ea6d7c4 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeTrackMapProvider.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeTrackMapProvider.kt @@ -24,14 +24,16 @@ import org.meshtastic.proto.Position /** * Provides an embeddable position-track map composable that renders a polyline with markers for the given [positions]. - * Unlike [LocalNodeMapScreenProvider], this does **not** include a Scaffold or AppBar — it is designed to be embedded - * inside another screen layout (e.g. the position-log adaptive layout). + * It has no Scaffold or AppBar, so it can be embedded inside another screen layout (e.g. the position-log adaptive + * layout). * * Supports optional synchronized selection: * - [selectedPositionTime]: the `Position.time` of the currently selected position (or `null` for no selection). When * non-null, the map should visually highlight the corresponding marker and center the camera on it. * - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so * the host can synchronize the card list. + * - [showAttribution]: whether the credit opens with the map or stays collapsed behind its own button. The embedded + * instance collapses it; a full-screen one does not. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. */ @@ -44,7 +46,8 @@ val LocalNodeTrackMapProvider = modifier: Modifier, selectedPositionTime: Int?, onPositionSelected: ((Int) -> Unit)?, + showAttribution: Boolean, ) -> Unit, > { - { _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } + { _, _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index 7277545907..149efdc24c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -65,12 +65,6 @@ expect fun rememberSaveFileLauncher( */ @Composable expect fun rememberOpenDocumentTreeLauncher(onTreeUriSelect: (CommonUri?) -> Unit): () -> Unit -/** - * Returns a suspend function that reads up to [maxChars] characters of text from a [CommonUri]. Returns `null` if the - * file is empty or cannot be read. - */ -@Composable expect fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? - /** Keeps the screen awake while [enabled] is true. No-op on platforms that don't support it. */ @Composable expect fun KeepScreenOn(enabled: Boolean) @@ -102,6 +96,9 @@ expect val bleScanRequiresLocationServices: Boolean */ @Composable expect fun isBluetoothDisabled(): Boolean +/** Returns whether the device has Bluetooth LE hardware at all, so a BLE surface is worth offering. */ +@Composable expect fun isBluetoothSupported(): Boolean + /** * Returns whether the device currently lacks any transport that can back the network-scan discovery (no active Wi-Fi, * Ethernet, or VPN). Cellular alone is **not** sufficient — a carrier uplink does not place the device on the same @@ -121,6 +118,12 @@ expect val bleScanRequiresLocationServices: Boolean */ @Composable expect fun rememberLocationPermissionState(): PermissionUiState +/** + * Like [rememberLocationPermissionState], but granted only with precise location. Use it where an approximate fix would + * be wrong rather than merely less useful, such as sharing the phone's position to the mesh. + */ +@Composable expect fun rememberPreciseLocationPermissionState(): PermissionUiState + /** * Returns the reactive [PermissionUiState] for the Bluetooth scan/connect permissions. On pre-Android-12 devices BLE * scanning is gated by the location permission, so the returned state delegates to [rememberLocationPermissionState]. diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt index 11125aa92c..662f80dfc7 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.model.util.channelIdentity import org.meshtastic.core.model.util.isChannelPlaceholder import org.meshtastic.core.model.util.toChannelReplacementPlan @@ -39,18 +40,16 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Position import kotlin.time.Duration.Companion.days -private const val SECONDS_TO_MILLIS = 1000L - @Composable fun Position.formatPositionTime(): String { val currentTime = nowMillis val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds - val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo + val isOlderThanSixMonths = time * TimeConstants.MS_PER_SEC < sixMonthsAgo val timeText = if (isOlderThanSixMonths) { stringResource(Res.string.unknown_age) } else { - DateFormatter.formatDateTime(time * SECONDS_TO_MILLIS) + DateFormatter.formatDateTime(time * TimeConstants.MS_PER_SEC) } return timeText } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt index 8ebf7e34a0..021b3730cb 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt @@ -33,31 +33,27 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.FirmwareUpdateNotice import org.meshtastic.core.model.FirmwareUpdateNoticePolicy import org.meshtastic.core.model.FirmwareUpdateTransport +import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FirmwareReleaseRepository +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.firmware_update_available -import org.meshtastic.core.resources.firmware_update_notification_android -import org.meshtastic.core.resources.firmware_update_notification_flasher -import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig @@ -105,7 +101,7 @@ class ConnectionsViewModel( private val deviceHardwareRepository: DeviceHardwareRepository, private val firmwareReleaseRepository: FirmwareReleaseRepository, private val radioPrefs: RadioPrefs, - private val notificationManager: NotificationManager, + private val serviceNotifications: MeshNotificationManager, ) : ViewModel() { private val scheduledFirmwareUpdateNotificationKeys = mutableSetOf() @@ -245,7 +241,8 @@ class ConnectionsViewModel( combine(firmwareUpdateInputs, localHardware) { inputs, hardware -> val state = inputs.connectionState if (state !is ConnectionState.Connected) return@combine null - val transport = inputs.address?.firstOrNull()?.toFirmwareUpdateTransport() ?: return@combine null + val transport = + DeviceAddress.parse(inputs.address)?.interfaceId?.toFirmwareUpdateTransport() ?: return@combine null val stableRelease = inputs.stableRelease ?: return@combine null val deviceHardware = hardware ?: return@combine null FirmwareUpdateCandidate( @@ -293,42 +290,7 @@ class ConnectionsViewModel( } .filterNotNull() .onEach { notice -> - val message = - when (notice.destination) { - org.meshtastic.core.model.FirmwareUpdateDestination.AndroidUpdate -> - getStringSuspend( - Res.string.firmware_update_notification_android, - notice.currentVersion, - notice.stableVersion, - ) - - org.meshtastic.core.model.FirmwareUpdateDestination.MeshtasticFlasher -> - getStringSuspend( - Res.string.firmware_update_notification_flasher, - notice.currentVersion, - notice.stableVersion, - ) - } - if ( - notificationManager.dispatch( - Notification( - id = notice.notificationKey.hashCode(), - title = getStringSuspend(Res.string.firmware_update_available), - message = message, - type = Notification.Type.Info, - category = Notification.Category.NodeEvent, - deepLinkUri = - if ( - notice.destination == - org.meshtastic.core.model.FirmwareUpdateDestination.AndroidUpdate - ) { - "meshtastic:///firmware/update" - } else { - "https://flasher.meshtastic.org" - }, - ), - ) - ) { + if (serviceNotifications.showFirmwareUpdateNotification(notice)) { scheduledFirmwareUpdateNotificationKeys += notice.notificationKey uiPrefs.recordFirmwareUpdateNotificationKey(notice.notificationKey) } @@ -353,9 +315,15 @@ private data class FirmwareUpdateCandidate( val transport: FirmwareUpdateTransport, ) -private fun Char.toFirmwareUpdateTransport(): FirmwareUpdateTransport? = when (this) { - 'x' -> FirmwareUpdateTransport.Bluetooth - 's' -> FirmwareUpdateTransport.Serial - 't' -> FirmwareUpdateTransport.Tcp - else -> null +private fun InterfaceId.toFirmwareUpdateTransport(): FirmwareUpdateTransport? = when (this) { + InterfaceId.BLUETOOTH -> FirmwareUpdateTransport.Bluetooth + + InterfaceId.SERIAL -> FirmwareUpdateTransport.Serial + + InterfaceId.TCP -> FirmwareUpdateTransport.Tcp + + InterfaceId.MOCK, + InterfaceId.NOP, + InterfaceId.REPLAY, + -> null } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt index 7798c7ec0d..e93121e44f 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt @@ -42,12 +42,12 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.EventFirmwareEdition import org.meshtastic.core.model.MeshActivity import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.TracerouteMapAvailability +import org.meshtastic.core.model.asDeviceVersion import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.model.util.dispatchMeshtasticUri @@ -59,15 +59,14 @@ import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.LockdownCoordinator import org.meshtastic.core.repository.LockdownPassphraseStore import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioController import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs -import org.meshtastic.core.repository.notificationId import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys @@ -102,7 +101,7 @@ class UIViewModel( private val eventFirmwareRepository: EventFirmwareRepository, private val firmwareUpdateStatusRepository: FirmwareUpdateStatusRepository, private val uiPrefs: UiPrefs, - private val notificationManager: NotificationManager, + private val serviceNotifications: MeshNotificationManager, packetRepository: PacketRepository, val alertManager: AlertManager, val snackbarManager: SnackbarManager, @@ -192,7 +191,7 @@ class UIViewModel( fun clearClientNotification(notification: ClientNotification) { serviceRepository.clearClientNotification() - notificationManager.cancel(notification.notificationId()) + serviceNotifications.clearClientNotification(notification) } val lockdownState = serviceRepository.lockdownState diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPaneUiTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPaneUiTest.kt index b83508a9bc..e8ff82d2b6 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPaneUiTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/AdaptiveTwoPaneUiTest.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.ui.component import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState @@ -25,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -118,4 +120,29 @@ class AdaptiveTwoPaneUiTest { val restoredX = handle.fetchSemanticsNode().boundsInRoot.center.x assertTrue(abs(restoredX - draggedX) < 3f, "divider was at $draggedX but came back at $restoredX") } + + @Test + fun panesRenderANewSlotLambda() = runComposeUiTest { + var swapped by mutableStateOf(false) + setContent { AppTheme { Box(modifier = Modifier.fillMaxSize()) { SwappablePanes(swapped = swapped) } } } + + onNodeWithText("first A").assertIsDisplayed() + onNodeWithText("second A").assertIsDisplayed() + + swapped = true + waitForIdle() + + onNodeWithText("first B").assertIsDisplayed() + onNodeWithText("second B").assertIsDisplayed() + } +} + +// Distinct lambda instances per slot, as a caller forwarding one of several slot parameters would pass. +@Composable +private fun SwappablePanes(swapped: Boolean) { + val firstA: @Composable ColumnScope.() -> Unit = { Text("first A") } + val firstB: @Composable ColumnScope.() -> Unit = { Text("first B") } + val secondA: @Composable ColumnScope.() -> Unit = { Text("second A") } + val secondB: @Composable ColumnScope.() -> Unit = { Text("second B") } + AdaptiveTwoPane(first = if (swapped) firstB else firstA, second = if (swapped) secondB else secondA) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/BuildNodeDescriptionTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/BuildNodeDescriptionTest.kt index 9ba609691b..9eedc09ad9 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/BuildNodeDescriptionTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/BuildNodeDescriptionTest.kt @@ -21,7 +21,6 @@ import kotlin.test.assertContains import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("MagicNumber") class BuildNodeDescriptionTest { private val testStrings = @@ -31,7 +30,7 @@ class BuildNodeDescriptionTest { favorite = "favorite", lastHeard = "last heard %s", role = "role %s", - hopsAway = "0 hops away", + hopsAway = "3 hops away", battery = "battery 0%", distanceAway = "%s away", signal = "signal %s", diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/DropDownPreferenceUiTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/DropDownPreferenceUiTest.kt new file mode 100644 index 0000000000..5fa013b7b5 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/DropDownPreferenceUiTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.proto.Config +import org.meshtastic.proto.ModuleConfig +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class DropDownPreferenceUiTest { + + @Test + fun deprecatedValueTheRadioHoldsStaysOnTheList() = runComposeUiTest { + setContent { + DropDownPreference( + title = "Bitrate", + enabled = true, + selectedItem = ModuleConfig.AudioConfig.Audio_Baud.CODEC2_700, + onItemSelected = {}, + ) + } + + onNodeWithText("CODEC2_700").assertIsDisplayed() + } + + @Test + fun valueTheSchemaNamesIsShownByItsLabel() = runComposeUiTest { + setContent { + DropDownPreference( + title = "Rebroadcast Mode", + enabled = true, + selectedItem = Config.DeviceConfig.RebroadcastMode.LOCAL_ONLY, + onItemSelected = {}, + ) + } + + onNodeWithText("Local Only").assertIsDisplayed() + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplayTabStateTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplayTabStateTest.kt new file mode 100644 index 0000000000..3206bfa379 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplayTabStateTest.kt @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import org.meshtastic.core.navigation.ConnectionsRoute +import org.meshtastic.core.navigation.ContactsRoute +import org.meshtastic.core.navigation.MapRoute +import org.meshtastic.core.navigation.MultiBackstack +import org.meshtastic.core.navigation.NodesRoute +import org.meshtastic.core.navigation.SettingsRoute +import org.meshtastic.core.navigation.rememberMultiBackstack +import org.meshtastic.core.ui.theme.AppTheme +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotSame +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@OptIn(ExperimentalTestApi::class) +class MeshtasticNavDisplayTabStateTest { + + private val probes = mutableMapOf() + + @Test + fun tabKeepsSavedStateAndViewModelsWhileAnotherTabIsShown() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + onNodeWithText("nodes saveable=0").performClick() + onNodeWithText("nodes saveable=1").assertIsDisplayed() + val nodesViewModel = probes.getValue("nodes") + assertEquals(1, nodesViewModel.clicks) + + runOnIdle { multiBackstack.navigateTopLevel(MapRoute.Map()) } + waitForIdle() + onNodeWithText("map tab").assertIsDisplayed() + + runOnIdle { multiBackstack.navigateTopLevel(NodesRoute.Nodes) } + waitForIdle() + + assertFalse(nodesViewModel.cleared, "leaving the tab cleared its ViewModel") + assertSame(nodesViewModel, probes.getValue("nodes")) + assertEquals(1, probes.getValue("nodes").clicks) + onNodeWithText("nodes saveable=1").assertIsDisplayed() + } + + @Test + fun poppedEntryDropsItsSavedStateAndViewModel() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + onNodeWithText("detail saveable=0").performClick() + onNodeWithText("detail saveable=1").assertIsDisplayed() + val detailViewModel = probes.getValue("detail") + + runOnIdle { multiBackstack.goBack() } + waitForIdle() + assertTrue(detailViewModel.cleared, "popping the entry kept its ViewModel") + + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + onNodeWithText("detail saveable=0").assertIsDisplayed() + assertNotSame(detailViewModel, probes.getValue("detail")) + } + + @Test + fun entryPoppedFromAHiddenTabDropsItsViewModel() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + val detailViewModel = probes.getValue("detail") + + runOnIdle { multiBackstack.navigateTopLevel(MapRoute.Map()) } + waitForIdle() + assertFalse(detailViewModel.cleared, "leaving the tab cleared its ViewModel") + + runOnIdle { multiBackstack.backStacks.getValue(NodesRoute.Nodes).removeLastOrNull() } + waitForIdle() + onNodeWithText("map tab").assertIsDisplayed() + assertTrue(detailViewModel.cleared, "an entry popped while its tab was hidden kept its ViewModel") + } + + // handleDeepLink switches tab and replaces its stack before the next composition. + @Test + fun deepLinkIntoAHiddenTabDropsTheEntriesItReplaces() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + val detailViewModel = probes.getValue("detail") + + runOnIdle { multiBackstack.navigateTopLevel(MapRoute.Map()) } + waitForIdle() + assertFalse(detailViewModel.cleared, "leaving the tab cleared its ViewModel") + + runOnIdle { multiBackstack.handleDeepLink(listOf(NodesRoute.Nodes)) } + waitForIdle() + onNodeWithText("nodes saveable=0").assertIsDisplayed() + assertTrue(detailViewModel.cleared, "an entry the deep link replaced kept its ViewModel") + } + + @Test + fun reselectingTheActiveTabDropsTheEntriesAboveItsRoot() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + onNodeWithText("nodes saveable=0").performClick() + val nodesViewModel = probes.getValue("nodes") + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + onNodeWithText("detail saveable=0").performClick() + onNodeWithText("detail saveable=1").assertIsDisplayed() + val detailViewModel = probes.getValue("detail") + + runOnIdle { multiBackstack.navigateTopLevel(NodesRoute.Nodes) } + waitForIdle() + + onNodeWithText("nodes saveable=1").assertIsDisplayed() + assertFalse(nodesViewModel.cleared, "reselecting the tab cleared its root's ViewModel") + assertTrue(detailViewModel.cleared, "reselecting the tab kept the dropped entry's ViewModel") + + runOnIdle { multiBackstack.activeBackStack.add(NodesRoute.NodeDetail(destNum = 1)) } + waitForIdle() + onNodeWithText("detail saveable=0").assertIsDisplayed() + assertNotSame(detailViewModel, probes.getValue("detail")) + } + + @Test + fun entryNavigatesOnItsOwnTabsStack() = runComposeUiTest { + lateinit var multiBackstack: MultiBackstack + setContent { multiBackstack = TabHost() } + + runOnIdle { multiBackstack.navigateTopLevel(MapRoute.Map()) } + waitForIdle() + onNodeWithText("map tab").performClick() + waitForIdle() + + assertEquals(2, multiBackstack.backStacks.getValue(MapRoute.Map()).size) + assertEquals(1, multiBackstack.backStacks.getValue(NodesRoute.Nodes).size) + } + + // Like the app hosts, the entry provider closes over the active tab's stack. + @Composable + private fun TabHost(): MultiBackstack { + val multiBackstack = rememberMultiBackstack(NodesRoute.Nodes) + val backStack = multiBackstack.activeBackStack + AppTheme { + MeshtasticNavDisplay( + multiBackstack = multiBackstack, + entryProvider = entryProvider { tabGraph(backStack) }, + ) + } + return multiBackstack + } + + // A plain graph function, as the feature graphs are: entries declared inline in a composable become remembered + // composable lambdas that the compiler updates in place, which would hide a stale back stack capture. + private fun EntryProviderScope.tabGraph(backStack: NavBackStack) { + entry { Probe(name = "nodes") } + entry { Probe(name = "detail") } + entry { + Button(onClick = { backStack.add(NodesRoute.NodeDetail(destNum = 1)) }) { Text("map tab") } + } + entry { Text("messages tab") } + entry { Text("settings tab") } + entry { Text("connections tab") } + } + + @Composable + private fun Probe(name: String) { + var saveable by rememberSaveable { mutableIntStateOf(0) } + val viewModel = viewModel { ProbeViewModel(createSavedStateHandle()) } + SideEffect { probes[name] = viewModel } + Button( + onClick = { + saveable++ + viewModel.clicks++ + }, + ) { + Text("$name saveable=$saveable") + } + } +} + +private class ProbeViewModel(private val handle: SavedStateHandle) : ViewModel() { + var cleared = false + private set + + var clicks: Int + get() = handle[CLICKS_KEY] ?: 0 + set(value) { + handle[CLICKS_KEY] = value + } + + override fun onCleared() { + cleared = true + } + + private companion object { + const val CLICKS_KEY = "clicks" + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ScreenViewTrackerTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ScreenViewTrackerTest.kt index 748ad3a849..c468ddbbd8 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ScreenViewTrackerTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ScreenViewTrackerTest.kt @@ -35,9 +35,9 @@ class ScreenViewTrackerTest { assertEquals( listOf( - "start:org.meshtastic.core.navigation.NodesRoute.Nodes:name=org.meshtastic.core.navigation.NodesRoute.Nodes", - "stop:org.meshtastic.core.navigation.NodesRoute.Nodes", - "start:org.meshtastic.core.navigation.NodeDetailRoute.DeviceMetrics:name=org.meshtastic.core.navigation.NodeDetailRoute.DeviceMetrics", + "start:NodesRoute.Nodes:name=NodesRoute.Nodes", + "stop:NodesRoute.Nodes", + "start:NodeDetailRoute.DeviceMetrics:name=NodeDetailRoute.DeviceMetrics", ), analytics.events, ) @@ -54,8 +54,8 @@ class ScreenViewTrackerTest { assertEquals( listOf( - "start:org.meshtastic.core.navigation.NodesRoute.Nodes:name=org.meshtastic.core.navigation.NodesRoute.Nodes", - "stop:org.meshtastic.core.navigation.NodesRoute.Nodes", + "start:NodesRoute.Nodes:name=NodesRoute.Nodes", + "stop:NodesRoute.Nodes", ), analytics.events, ) @@ -70,9 +70,7 @@ class ScreenViewTrackerTest { tracker.onCurrentKeyChanged(NodeDetailRoute.DeviceMetrics(destNum = 2)) assertEquals( - listOf( - "start:org.meshtastic.core.navigation.NodeDetailRoute.DeviceMetrics:name=org.meshtastic.core.navigation.NodeDetailRoute.DeviceMetrics", - ), + listOf("start:NodeDetailRoute.DeviceMetrics:name=NodeDetailRoute.DeviceMetrics"), analytics.events, ) } @@ -87,8 +85,8 @@ class ScreenViewTrackerTest { assertEquals( listOf( - "start:org.meshtastic.core.navigation.NodesRoute.Nodes:name=org.meshtastic.core.navigation.NodesRoute.Nodes", - "stop:org.meshtastic.core.navigation.NodesRoute.Nodes", + "start:NodesRoute.Nodes:name=NodesRoute.Nodes", + "stop:NodesRoute.Nodes", ), analytics.events, ) diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/theme/LinkColorTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/theme/LinkColorTest.kt new file mode 100644 index 0000000000..212be86782 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/theme/LinkColorTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import org.meshtastic.core.ui.component.NODE_TINT_NORMAL +import kotlin.test.Test +import kotlin.test.assertTrue + +/** Pins the Link colour at WCAG AA text contrast on each scheme's surface, which no single blue manages. */ +class LinkColorTest { + + @Test + fun linkMeetsAaTextContrastOnTheLightSurface() { + assertAaText(lightColorScheme(surface = surfaceLight).link, surfaceLight) + } + + @Test + fun linkMeetsAaTextContrastOnTheLightDialogBackground() { + assertAaText(lightColorScheme(surface = surfaceLight).link, surfaceContainerHighLight) + } + + @Test + fun linkMeetsAaTextContrastOnTheDarkSurface() { + assertAaText(darkColorScheme(surface = surfaceDark).link, surfaceDark) + } + + @Test + fun linkMeetsAaTextContrastOnTheDarkDialogBackground() { + assertAaText(darkColorScheme(surface = surfaceDark).link, surfaceContainerHighDark) + } + + @Test + fun linkMeetsAaTextContrastOnEveryNodeTintedCardInBothThemes() { + // A message card is the Card container washed toward the sender's node colour, which can be any RGB. + val nodeColors = + (0..255 step 17).flatMap { r -> + (0..255 step 17).flatMap { g -> (0..255 step 17).map { b -> Color(r, g, b) } } + } + val themes = + listOf( + lightColorScheme(surface = surfaceLight).link to surfaceContainerHighestLight, + darkColorScheme(surface = surfaceDark).link to surfaceContainerHighestDark, + ) + themes.forEach { (link, card) -> + nodeColors.forEach { node -> assertAaText(link, lerp(card, node, NODE_TINT_NORMAL)) } + } + } + + private fun assertAaText(link: Color, surface: Color) { + val ratio = contrastRatio(link, surface) + assertTrue(ratio >= MIN_TEXT_CONTRAST, "link $link on $surface is $ratio:1, below $MIN_TEXT_CONTRAST:1") + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/WriteExportTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/WriteExportTest.kt new file mode 100644 index 0000000000..8590f0cb09 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/WriteExportTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.util + +import kotlinx.coroutines.test.runTest +import okio.Buffer +import okio.BufferedSink +import okio.BufferedSource +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.repository.FileService +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WriteExportTest { + + private class RecordingFileService(private val writeSucceeds: Boolean = true) : FileService { + val written = Buffer() + var writes = 0 + private set + + override suspend fun write(uri: CommonUri, block: suspend (BufferedSink) -> Unit): Boolean { + writes++ + if (!writeSucceeds) return false + block(written) + return true + } + + override suspend fun read(uri: CommonUri, block: suspend (BufferedSource) -> Unit): Boolean = false + } + + private val uri = CommonUri.parse("file:///tmp/export.txt") + + @Test + fun `produced bytes are written and reported as exported`() = runTest { + val fileService = RecordingFileService() + + val exported = writeExport(fileService, uri) { "log line".encodeToByteArray() } + + assertTrue(exported) + assertEquals("log line", fileService.written.readUtf8()) + } + + @Test + fun `nothing to export is reported as a failure without opening the file`() = runTest { + val fileService = RecordingFileService() + + val exported = writeExport(fileService, uri) { null } + + assertFalse(exported) + assertEquals(0, fileService.writes) + } + + @Test + fun `content that throws is reported as a failure without opening the file`() = runTest { + val fileService = RecordingFileService() + + val exported = writeExport(fileService, uri) { error("log query failed") } + + assertFalse(exported) + assertEquals(0, fileService.writes) + } + + @Test + fun `a failed write is reported as a failure`() = runTest { + val fileService = RecordingFileService(writeSucceeds = false) + + val exported = writeExport(fileService, uri) { "log line".encodeToByteArray() } + + assertFalse(exported) + assertEquals(1, fileService.writes) + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt index dee52c9a89..0c68ae9e02 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt @@ -31,25 +31,18 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.common.util.safeCatchingAll -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.FirmwareUpdateDestination import org.meshtastic.core.repository.ConnectionIdentity import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.Notification -import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.firmware_update_available -import org.meshtastic.core.resources.firmware_update_notification_android -import org.meshtastic.core.resources.firmware_update_notification_flasher -import org.meshtastic.core.resources.getString import org.meshtastic.core.testing.FakeDeviceHardwareRepository import org.meshtastic.core.testing.FakeFirmwareReleaseRepository +import org.meshtastic.core.testing.FakeMeshNotificationManager import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioPrefs import org.meshtastic.core.testing.FakeServiceRepository @@ -80,26 +73,13 @@ class ConnectionsViewModelTest { private val deviceHardwareRepository = FakeDeviceHardwareRepository() private val firmwareReleaseRepository = FakeFirmwareReleaseRepository() private val radioPrefs = FakeRadioPrefs() - private val dispatchedNotifications = mutableListOf() - private var notificationsCanBeScheduled = true - private val notificationManager = - object : NotificationManager { - override suspend fun dispatch(notification: Notification): Boolean { - if (notificationsCanBeScheduled) dispatchedNotifications += notification - return notificationsCanBeScheduled - } - - override fun cancel(id: Int) = Unit - - override fun cancelAll() = Unit - } + private val serviceNotifications = FakeMeshNotificationManager() + private val postedNotices + get() = serviceNotifications.firmwareUpdateNotices @BeforeTest fun setUp() { - warmFirmwareNotificationStrings() Dispatchers.setMain(testDispatcher) - dispatchedNotifications.clear() - notificationsCanBeScheduled = true every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) every { nodeManager.connectionIdentity } returns connectionIdentity @@ -121,7 +101,7 @@ class ConnectionsViewModelTest { deviceHardwareRepository = deviceHardwareRepository, firmwareReleaseRepository = firmwareReleaseRepository, radioPrefs = radioPrefs, - notificationManager = notificationManager, + serviceNotifications = serviceNotifications, ) @AfterTest @@ -363,18 +343,70 @@ class ConnectionsViewModelTest { assertEquals("2.7.0", notice.currentVersion) assertEquals("2.8.0", notice.stableVersion) assertEquals(FirmwareUpdateDestination.AndroidUpdate, notice.destination) - assertEquals(1, dispatchedNotifications.size) + assertEquals(listOf(notice), postedNotices) assertEquals(setOf(notice.notificationKey), uiPrefs.firmwareUpdateNotificationKeys.value) - assertEquals("Firmware update available", dispatchedNotifications.single().title) - assertEquals(Notification.Type.Info, dispatchedNotifications.single().type) - assertEquals("meshtastic:///firmware/update", dispatchedNotifications.single().deepLinkUri) + } + + @Test + fun `firmware notice for flasher-only hardware is still posted`() = runTest { + val target = "tbeam" + deviceHardwareRepository.setHardware( + hwModel = HardwareModel.TBEAM.value, + target = target, + device = DeviceHardware(architecture = "esp32", platformioTarget = target), + ) + nodeRepository.setMyId("!local") + nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) + // ESP32 over serial is not updatable in-app, so the notice's destination is the flasher. + radioPrefs.setDevAddr("s:connected") + firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf(target)) + firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0")) + serviceRepository.setConnectionState(ConnectionState.Connected) + + advanceUntilIdle() + + val notice = assertNotNull(viewModel.firmwareUpdateNotice.value) + assertEquals(FirmwareUpdateDestination.MeshtasticFlasher, notice.destination) + assertEquals(listOf(notice), postedNotices) + } + + @Test + fun `a device saved with the legacy bang prefix gets the firmware update notice`() = runTest { + val target = "tbeam" + deviceHardwareRepository.setHardware( + hwModel = HardwareModel.TBEAM.value, + target = target, + device = DeviceHardware(architecture = "esp32", platformioTarget = target), + ) + nodeRepository.setMyId("!local") + nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) + radioPrefs.setDevAddr("!AA:BB:CC:DD:EE:FF") + firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf(target)) + firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0")) + serviceRepository.setConnectionState(ConnectionState.Connected) + + advanceUntilIdle() + + assertNotNull(viewModel.firmwareUpdateNotice.value) } @Test fun `does not persist firmware notification dedupe when scheduling is unavailable`() = runTest { val hardwareModel = HardwareModel.TBEAM.value val target = "tbeam" - notificationsCanBeScheduled = false + serviceNotifications.acceptsFirmwareUpdate = false deviceHardwareRepository.setHardware( hwModel = hardwareModel, target = target, @@ -396,7 +428,7 @@ class ConnectionsViewModelTest { advanceUntilIdle() assertNotNull(viewModel.firmwareUpdateNotice.value) - assertEquals(emptyList(), dispatchedNotifications) + assertEquals(emptyList(), postedNotices) assertEquals(emptySet(), uiPrefs.firmwareUpdateNotificationKeys.value) } @@ -451,7 +483,7 @@ class ConnectionsViewModelTest { advanceUntilIdle() assertEquals(null, viewModel.firmwareUpdateNotice.value) - assertEquals(emptyList(), dispatchedNotifications) + assertEquals(emptyList(), postedNotices) } /** @@ -466,18 +498,4 @@ class ConnectionsViewModelTest { fun `RECONNECTING_PROGRESS_TEXT pins the cross-track literal value`() { assertEquals("Reconnecting\u2026", ServiceRepository.RECONNECTING_PROGRESS_TEXT) } - - /** - * From CMP 1.12 compose resources load each string once on a library-owned `Dispatchers.Default` scope, which - * `advanceUntilIdle` cannot drain, so the notification dispatch lands after the assertions. Pre-loading keeps the - * path inside virtual time on any CMP version; must run before `setMain`. Best-effort: a warm-up that cannot load - * (skiko's static initializer on the desktop test classpath) must leave the suite as it was, not fail every test. - */ - private fun warmFirmwareNotificationStrings() { - safeCatchingAll { - getString(Res.string.firmware_update_available) - getString(Res.string.firmware_update_notification_android, "", "") - getString(Res.string.firmware_update_notification_flasher, "", "") - } - } } diff --git a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt new file mode 100644 index 0000000000..9f0f1cf5c5 --- /dev/null +++ b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +@Suppress("UNUSED_PARAMETER") +actual fun BoxScope.ListScrollbar(state: LazyListState, modifier: Modifier) = Unit diff --git a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt index e9d775da6f..5d616cc88c 100644 --- a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt +++ b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt @@ -51,8 +51,6 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT @Composable actual fun rememberOpenDocumentTreeLauncher(onTreeUriSelect: (CommonUri?) -> Unit): () -> Unit = {} -@Composable actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { _, _ -> null } - @Composable actual fun KeepScreenOn(enabled: Boolean) { // No-op iOS stub. @@ -70,6 +68,8 @@ actual val bleScanRequiresLocationServices: Boolean = false @Composable actual fun isBluetoothDisabled(): Boolean = false +@Composable actual fun isBluetoothSupported(): Boolean = true + @Composable actual fun isWifiUnavailable(): Boolean = false @Composable @@ -81,6 +81,8 @@ actual fun SetScreenBrightness(brightness: Float) { @Composable actual fun rememberLocationPermissionState(): PermissionUiState = grantedPermissionUiState() +@Composable actual fun rememberPreciseLocationPermissionState(): PermissionUiState = grantedPermissionUiState() + @Composable actual fun rememberBluetoothPermissionState(): PermissionUiState = grantedPermissionUiState() @Composable actual fun rememberNotificationPermissionState(): PermissionUiState = grantedPermissionUiState() diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt similarity index 51% rename from feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt rename to core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt index c248d2f57d..d0e708c688 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt +++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/ListScrollbar.kt @@ -14,29 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.node.component +package org.meshtastic.core.ui.component -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.channel_label -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Tsunami @Composable -fun ChannelInfo( - channel: Int, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Tsunami, - contentDescription = stringResource(Res.string.channel_label), - text = channel.toString(), - contentColor = contentColor, +actual fun BoxScope.ListScrollbar(state: LazyListState, modifier: Modifier) { + VerticalScrollbar( + adapter = rememberScrollbarAdapter(state), + modifier = modifier.align(Alignment.CenterEnd).fillMaxHeight(), + reverseLayout = state.layoutInfo.reverseLayout, ) } diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index 7b7781361c..46a943c3a8 100644 --- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -23,10 +23,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalClipboard import co.touchlab.kermit.Logger import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.common.util.ioDispatcher import java.awt.Desktop import java.awt.FileDialog import java.awt.Frame @@ -55,8 +53,8 @@ actual fun rememberShowToastResource(): suspend (StringResource) -> Unit = { _ - /** JVM stub — map opening is not available on Desktop. */ @Composable -actual fun rememberOpenMap(): (latitude: Double, longitude: Double, label: String) -> Unit = { lat, lon, label -> - Logger.i { "Open map: $lat, $lon ($label)" } +actual fun rememberOpenMap(): (latitude: Double, longitude: Double, label: String) -> Unit = { _, _, _ -> + Logger.i { "Open map requested; not available on Desktop" } } /** JVM stub — URL opening via Desktop browse API. */ @@ -112,29 +110,6 @@ actual fun rememberOpenDocumentTreeLauncher(onTreeUriSelect: (CommonUri?) -> Uni } } -/** JVM — Reads text from a file URI. */ -@Composable -actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars -> - withContext(ioDispatcher) { - @Suppress("TooGenericExceptionCaught") - try { - val file = File(URI(uri.toString())) - if (file.exists()) { - file.bufferedReader().use { reader -> - val buffer = CharArray(maxChars) - val read = reader.read(buffer) - if (read > 0) String(buffer, 0, read) else null - } - } else { - null - } - } catch (e: Exception) { - Logger.e(e) { "Failed to read text from URI: $uri" } - null - } - } -} - /** JVM no-op — Keep screen on is not applicable on Desktop. */ @Composable actual fun KeepScreenOn(enabled: Boolean) { @@ -162,6 +137,8 @@ actual val bleScanRequiresLocationServices: Boolean = false /** JVM — Bluetooth adapter state is not surfaced on Desktop. */ @Composable actual fun isBluetoothDisabled(): Boolean = false +@Composable actual fun isBluetoothSupported(): Boolean = true + /** JVM — local-network availability is not gated on Desktop. */ @Composable actual fun isWifiUnavailable(): Boolean = false @@ -172,6 +149,9 @@ actual fun rememberOpenAppSettings(): () -> Unit = { Logger.w { "App settings no /** JVM — Desktop does not gate location behind a runtime permission. */ @Composable actual fun rememberLocationPermissionState(): PermissionUiState = grantedPermissionUiState() +/** Desktop has no runtime gate on location precision either. */ +@Composable actual fun rememberPreciseLocationPermissionState(): PermissionUiState = grantedPermissionUiState() + /** JVM — Desktop does not gate Bluetooth behind a runtime permission. */ @Composable actual fun rememberBluetoothPermissionState(): PermissionUiState = grantedPermissionUiState() diff --git a/crowdin.yml b/crowdin.yml index 21d692ee8c..72f84f0365 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -10,6 +10,14 @@ files: escape_quotes: 0 escape_special_characters: 0 type: android + # Generated by ./gradlew :schema-strings:sync from the protobufs field metadata; the English is the schema's. + - source: /**/composeResources/values/schema_strings.xml + translation: /%original_path%-%two_letters_code%/schema_strings.xml + translate_attributes: 0 + content_segmentation: 0 + escape_quotes: 0 + escape_special_characters: 0 + type: android # Crowdin's %locale% is not Google Play's language set: it renders regions Play rejects # (ar-SA), `srp` for Serbian, and the modern Hebrew code where Play wants the legacy one. # Nothing validates this - supply uploads every directory it finds and fastlane documents diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 78470dc1be..640b57d8bf 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -16,11 +16,18 @@ */ import dev.detekt.gradle.Detekt +import org.gradle.api.file.FileSystemOperations +import org.gradle.process.ExecOperations import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask +import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.meshtastic.buildlogic.configureGraphTasks +import org.meshtastic.buildlogic.kotlinWarningsAsErrors import org.meshtastic.buildlogic.maplibreDesktopRuntime import org.meshtastic.buildlogic.resolveVersionInfo +import java.io.File +import javax.inject.Inject plugins { alias(libs.plugins.kotlin.jvm) @@ -108,7 +115,8 @@ kotlin { } compilerOptions { jvmTarget.set(JvmTarget.JVM_25) - freeCompilerArgs.add("-jvm-default=no-compatibility") + jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY) + allWarningsAsErrors.set(kotlinWarningsAsErrors) } } @@ -275,6 +283,59 @@ compose.desktop { } } +// ── .deb dependency names ──────────────────────────────────────────────────── +// jpackage names each dependency by running `dpkg -S` on the build host, so a deb built on +// Ubuntu 24.04 requires `libasound2t64` and `libpng16-16t64`, which Debian 12 Bookworm and +// Raspberry Pi OS do not have — the package is uninstallable there. Stripping the suffix +// resolves on both: Bookworm ships the plain names, and the t64 packages Provide them. +// +// Rewriting the built archive is the only lever. jpackage's --linux-package-deps maps to +// `additionalDependencies` (add-only, it cannot drop a detected name), and the Compose plugin +// owns --resource-dir and clears it inside the task, so a custom control template cannot be +// injected. This runs as the deb task's own final action rather than a separate task, which +// would have to declare jpackage's output directory as its input and mutate it in place. +abstract class RewriteDebDependencyNames : Action { + @get:Inject abstract val execOps: ExecOperations + + @get:Inject abstract val fileOps: FileSystemOperations + + // Held here, not at script scope: a task action that referenced a script property would + // capture the build script itself and the configuration cache would reject it. + private val dependencyLine = Regex("^(?:Depends|Pre-Depends|Recommends):.*$", RegexOption.MULTILINE) + private val t64Suffix = Regex("(lib[a-z0-9.+-]*)t64(?=[,( ]|$)") + private val t64Survivor = Regex("\\blib[a-z0-9.+-]*t64\\b") + + override fun execute(task: Task) { + val debs = (task as AbstractJPackageTask).destinationDir.get().asFile.listFiles { f -> f.extension == "deb" } + debs?.forEach { deb -> rewrite(task, deb) } + } + + private fun rewrite(task: Task, deb: File) { + val work = File.createTempFile("deb-", "").apply { delete() } + try { + execOps.exec { commandLine("dpkg-deb", "-R", deb.absolutePath, work.absolutePath) } + val control = work.resolve("DEBIAN/control") + val rewritten = + dependencyLine.replace(control.readText()) { line -> + t64Suffix.replace(line.value) { it.groupValues[1] } + } + val survivor = dependencyLine.findAll(rewritten).firstOrNull { t64Survivor.containsMatchIn(it.value) } + check(survivor == null) { "t64 dependency name survived the rewrite in ${deb.name}: ${survivor?.value}" } + control.writeText(rewritten) + execOps.exec { commandLine("dpkg-deb", "-b", "--root-owner-group", work.absolutePath, deb.absolutePath) } + task.logger.lifecycle("Rewrote t64 dependency names in ${deb.name}") + } finally { + fileOps.delete { delete(work) } + } + } +} + +tasks.withType().configureEach { + if (targetFormat == TargetFormat.Deb) { + doLast(objects.newInstance()) + } +} + dependencies { implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) @@ -362,7 +423,6 @@ dependencies { implementation(libs.androidx.datastore) implementation(libs.androidx.room.runtime) implementation(libs.androidx.sqlite.bundled) - implementation(libs.kotlinx.collections.immutable) implementation(libs.jna) diff --git a/desktopApp/detekt-baseline.xml b/desktopApp/detekt-baseline.xml index 225cfe343e..eb207395c0 100644 --- a/desktopApp/detekt-baseline.xml +++ b/desktopApp/detekt-baseline.xml @@ -2,12 +2,8 @@ - LambdaParameterInRestartableEffect:Main.kt:onReady: () -> Unit - ModifierMissing:DesktopMainScreen.kt:@Composable fun DesktopMainScreen - ViewModelForwarding:DesktopMainScreen.kt:MeshtasticAppShell( multiBackstack = multiBackstack, uiViewModel = uiViewModel, hostModifier = Modifier.padding(bottom = 24.dp), ) { MeshtasticNavigationSuite( multiBackstack = multiBackstack, uiViewModel = uiViewModel, modifier = Modifier.fillMaxSize(), ) { val provider = entryProvider<NavKey> { desktopNavGraph(backStack, uiViewModel, multiBackstack) } MeshtasticNavDisplay( multiBackstack = multiBackstack, entryProvider = provider, modifier = Modifier.fillMaxSize(), ) } } - ViewModelForwarding:DesktopMainScreen.kt:MeshtasticNavigationSuite( multiBackstack = multiBackstack, uiViewModel = uiViewModel, modifier = Modifier.fillMaxSize(), ) { val provider = entryProvider<NavKey> { desktopNavGraph(backStack, uiViewModel, multiBackstack) } MeshtasticNavDisplay( multiBackstack = multiBackstack, entryProvider = provider, modifier = Modifier.fillMaxSize(), ) } - ViewModelForwarding:Main.kt:DesktopMainScreen(uiViewModel, multiBackstack) - ViewModelForwarding:Main.kt:MeshtasticDesktopApp(uiViewModel, isDarkTheme, contrastLevel) - ViewModelForwarding:Main.kt:MeshtasticWindow(uiViewModel, isDarkTheme, contrastLevel, appIcon, windowState) { isAppVisible = false } + UnnecessaryLaunchedEffect:DesktopTracerouteMap.kt:LaunchedEffect + UnnecessaryLaunchedEffect:Main.kt:LaunchedEffect + UnusedPrivateProperty:MacOSNotificationSender.kt:MacOSNotificationSender$unused: Unit = Unit diff --git a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml index 06cda696ec..88d4473f26 100644 --- a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml +++ b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml @@ -75,30 +75,35 @@ Track every node on the mesh at a glance - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png + https://github.com/meshtastic/Meshtastic-Android/releases/download/v2.8.3/meshtastic-desktop-01-nodes.png Chat with your mesh, channel by channel - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png + https://github.com/meshtastic/Meshtastic-Android/releases/download/v2.8.3/meshtastic-desktop-02-messages.png See the whole mesh on a map - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png + https://github.com/meshtastic/Meshtastic-Android/releases/download/v2.8.3/meshtastic-desktop-03-map.png Connect over Bluetooth, USB, or Wi-Fi - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png + https://github.com/meshtastic/Meshtastic-Android/releases/download/v2.8.3/meshtastic-desktop-04-connections.png Full radio and module configuration - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png + https://github.com/meshtastic/Meshtastic-Android/releases/download/v2.8.3/meshtastic-desktop-05-settings.png - + -

Stability and reliability fixes.

+

Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes.

+
+
+ + +

Messaging: swipe to reply, double-tap reactions, per-contact drafts, pinned and unread conversations, bubbles, proven delivery receipts. A Units setting. One search bar for nodes, messages, settings and docs. Settings take labels, units and firmware gates from the schema. Map layer opacity and role filters. Lightning, particulate and soil telemetry. Tap phones to share a contact or channel. KML import works again.

diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png index 69c7bdf5ba..2be20a63e7 100644 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png index 7ce1027754..8bd32bf279 100644 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png index d72654355e..80e1001575 100644 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png index 6b75454c48..5224041e6e 100644 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png index 1ae722fff4..cb77a29aec 100644 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png differ diff --git a/desktopApp/proguard-rules.pro b/desktopApp/proguard-rules.pro index 5cf56f7760..36078b977b 100644 --- a/desktopApp/proguard-rules.pro +++ b/desktopApp/proguard-rules.pro @@ -55,6 +55,23 @@ -keep class * implements com.sun.jna.Callback { *; } -keep class * extends com.sun.jna.Structure { *; } +# ---- MapLibre's Panama FFI bindings - upcalls resolved by MethodHandle ------ +# Every callback maplibre-native makes back into Kotlin is an upcall stub built +# from a MethodHandle the bindings look up by name at : `upcallHandle` +# findVirtual's "apply" on each `mln_*_callback$Function`, and each callback +# *State class findVirtual's its own "invoke". Nothing calls either statically, +# so the shrinker empties the nine interfaces and drops the four methods, and +# the lookup fails on the first map composed: +# NoSuchMethodException: no such method: ...mln_log_callback$Function +# .apply(MemorySegment,int,int,long,MemorySegment)int/invokeInterface +# rethrown as "Could not configure MapLibre's offline runtime" (#7286). Same +# shape as the JNA callbacks above: a vtable only native code ever calls. +# +# Android binds through JavaCPP/JNI instead, and that AAR ships the consumer +# rules R8 needs. The JVM bindings jar ships none. +-keep class org.maplibre.nativeffi.** { *; } +-keep interface org.maplibre.nativeffi.** { *; } + # ---- jSerialComm Android stubs (cross-platform serial library) -------------- # jSerialComm bundles Android shims that reference android.* classes; harmless # on JVM/desktop but ProGuard fails the build on unresolved program classes diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeMapScreenProvider.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopLogging.kt similarity index 53% rename from core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeMapScreenProvider.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopLogging.kt index 7e54003a52..3573bbf6f4 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNodeMapScreenProvider.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopLogging.kt @@ -14,18 +14,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.ui.util +package org.meshtastic.desktop -import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf -import org.meshtastic.core.ui.component.PlaceholderScreen +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.platformLogWriter +import org.meshtastic.core.common.log.InMemoryLogBuffer /** - * Provides the platform-specific Map Screen for a Node (e.g. Google Maps or OSMDroid on Android). On Desktop or JVM - * targets where native maps aren't available yet, it falls back to a [PlaceholderScreen]. + * Sends Kermit output to the console and to [InMemoryLogBuffer], which the Debug screen views and exports. Release + * builds keep Info and above, as Android release does; debug builds keep every level. */ -@Suppress("Wrapping") -val LocalNodeMapScreenProvider = - compositionLocalOf<@Composable (destNum: Int, onNavigateUp: () -> Unit) -> Unit> { - { destNum, _ -> PlaceholderScreen("Node Map ($destNum)") } - } +internal fun installDesktopLogging(isDebug: Boolean) { + Logger.setMinSeverity(if (isDebug) Severity.Verbose else Severity.Info) + Logger.setLogWriters(listOf(platformLogWriter(), InMemoryLogBuffer)) +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt index 09e792a754..89a9fbd28f 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt @@ -68,6 +68,7 @@ class DesktopNotificationManager( Notification.Category.MeshBeacon -> prefs.nodeEventsEnabled.value Notification.Category.Battery -> prefs.lowBatteryEnabled.value Notification.Category.Alert -> true + Notification.Category.Client -> true Notification.Category.Service -> true } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt index 84681d7ace..e26ea7da7c 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -17,6 +17,7 @@ package org.meshtastic.desktop import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -26,6 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -51,8 +53,8 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.isTraySupported import androidx.compose.ui.window.rememberTrayState import androidx.compose.ui.window.rememberWindowState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger -import co.touchlab.kermit.platformLogWriter import coil3.ImageLoader import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory @@ -65,6 +67,7 @@ import coil3.svg.SvgDecoder import coil3.util.DebugLogger import io.ktor.client.HttpClient import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext import okio.Path.Companion.toPath import org.jetbrains.compose.resources.decodeToSvgPainter import org.jetbrains.compose.resources.getString @@ -77,9 +80,11 @@ import org.koin.plugin.module.dsl.startKoin import org.maplibre.compose.desktop.ProvideMapPresentationHost import org.maplibre.compose.desktop.rememberAwtComposeMapPresentationHost import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.common.log.InMemoryLogBuffer +import org.meshtastic.core.common.state.LaunchOptions import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.database.desktopDataDir +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.navigation.MultiBackstack import org.meshtastic.core.navigation.SettingsRoute import org.meshtastic.core.navigation.TopLevelDestination @@ -93,6 +98,7 @@ import org.meshtastic.core.resources.desktop_tray_tooltip import org.meshtastic.core.resources.desktop_update_available_message import org.meshtastic.core.resources.desktop_update_available_title import org.meshtastic.core.resources.desktop_update_download +import org.meshtastic.core.service.MeshLogCleanup import org.meshtastic.core.service.MeshServiceOrchestrator import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.LocalDiscoveryMapProvider @@ -102,7 +108,9 @@ import org.meshtastic.core.ui.util.LocalMapMainScreenProvider import org.meshtastic.core.ui.util.LocalMapViewProvider import org.meshtastic.core.ui.util.LocalNodeTrackMapProvider import org.meshtastic.core.ui.util.LocalSitePlannerAvailable +import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider import org.meshtastic.core.ui.util.LocalTracerouteMapProvider +import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets import org.meshtastic.core.ui.util.rememberOpenUrl import org.meshtastic.core.ui.viewmodel.UIViewModel import org.meshtastic.desktop.data.DesktopPreferencesDataSource @@ -126,6 +134,9 @@ import coil3.util.Logger as CoilLogger private const val MEMORY_CACHE_MAX_BYTES = 64L * 1024L * 1024L // 64 MiB private const val DISK_CACHE_MAX_BYTES = 32L * 1024L * 1024L // 32 MiB +/** Debug builds only: apply a `connections` deep link from the command line without the trust dialog. */ +private const val SKIP_CONNECT_CONFIRM_ARG = "--skip-connect-confirm" + /** * Loads an SVG from JVM classpath resources and returns a [Painter]. * @@ -155,18 +166,22 @@ fun main(args: Array) { // No MapLibre.configure() call: the first map applies a default cache configuration process-wide. application(exitProcessOnExit = false) { val koinApp = remember { - // Keep console output and also capture into the in-memory buffer the Debug screen views/exports. - Logger.setLogWriters(listOf(platformLogWriter(), InMemoryLogBuffer)) + installDesktopLogging(isDebug = DesktopBuildConfig.IS_DEBUG) Logger.i { "Meshtastic Desktop — Starting" } startKoin {} + .also { app -> + if (DesktopBuildConfig.IS_DEBUG && SKIP_CONNECT_CONFIRM_ARG in args) { + app.koin.get().skipDeepLinkConfirmation = true + } + } } LaunchedEffect(Unit) { publishExitApplication(::exitApplication) } - val systemLocale = remember { Locale.getDefault() } val uiViewModel = remember { koinApp.koin.get() } - val httpClient = remember { koinApp.koin.get() } - DeepLinkHandler(args, uiViewModel) + DeepLinkHandler(args, uiViewModel, remember { koinApp.koin.get() }) MeshServiceLifecycle() + // Desktop has no WorkManager, so the hourly mesh log cleanup lives as long as the application composition. + LaunchedEffect(Unit) { withContext(ioDispatcher) { koinApp.koin.get().runHourly() } } ThemeAndLocaleProvider(uiViewModel) } @@ -188,7 +203,11 @@ fun main(args: Array) { /** Processes deep-link URIs from CLI arguments and OS-level URI handlers. */ @Composable -private fun ApplicationScope.DeepLinkHandler(args: Array, uiViewModel: UIViewModel) { +private fun ApplicationScope.DeepLinkHandler( + args: Array, + uiViewModel: UIViewModel, + launchOptions: LaunchOptions, +) { LaunchedEffect(args) { args.forEach { arg -> if ( @@ -206,6 +225,8 @@ private fun ApplicationScope.DeepLinkHandler(args: Array, uiViewModel: U LaunchedEffect(Unit) { if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_URI)) { Desktop.getDesktop().setOpenURIHandler { event -> + // The launch switch covers the links this process was started with, never one the OS hands over later. + launchOptions.skipDeepLinkConfirmation = false val uriStr = event.uri.toString() uiViewModel.handleDeepLink(CommonUri.parse(uriStr)) { Logger.e { "Invalid URI from OS: $uriStr" } } } @@ -352,6 +373,7 @@ private fun WindowBoundsManager( windowState: WindowState, onReady: () -> Unit, ) { + val currentOnReady by rememberUpdatedState(onReady) LaunchedEffect(Unit) { val initialWidth = desktopPrefs.windowWidth.first() val initialHeight = desktopPrefs.windowHeight.first() @@ -366,7 +388,7 @@ private fun WindowBoundsManager( WindowPosition(Alignment.Center) } - onReady() + currentOnReady() snapshotFlow { val x = if (windowState.position.isSpecified) windowState.position.x.value else Float.NaN @@ -396,7 +418,7 @@ private fun ApplicationScope.MeshtasticWindow( val multiBackstack = rememberMultiBackstack( // Land on Connections for first-run / no-device-selected; otherwise on Nodes. - if (uiViewModel.currentDeviceAddressFlow.value.let { it.isNullOrBlank() || it == "n" }) { + if (DeviceAddress.parse(uiViewModel.currentDeviceAddressFlow.value) == null) { TopLevelDestination.Connect.route } else { TopLevelDestination.Nodes.route @@ -411,7 +433,7 @@ private fun ApplicationScope.MeshtasticWindow( visible = visible, onPreviewKeyEvent = { event -> handleKeyboardShortcut(event, multiBackstack, ::exitApplication) }, ) { - val eventEdition by uiViewModel.eventEdition.collectAsState() + val eventEdition by uiViewModel.eventEdition.collectAsStateWithLifecycle() CoilImageLoaderSetup() // Each window hands MapLibre its own GPU context; the map composites into Compose from there. @@ -435,13 +457,14 @@ private fun ApplicationScope.MeshtasticWindow( }, LocalInlineMapProvider provides { node, modifier -> MapLibreInlineMap(node, modifier) }, LocalNodeTrackMapProvider provides - { destNum, positions, modifier, selectedPositionTime, onPositionSelect -> + { destNum, positions, modifier, selectedPositionTime, onPositionSelect, showAttribution -> MapLibreNodeTrackMap( destNum = destNum, positions = positions, modifier = modifier, selectedPositionTime = selectedPositionTime, onPositionSelect = onPositionSelect, + showAttribution = showAttribution, ) }, LocalDiscoveryMapProvider provides @@ -452,6 +475,9 @@ private fun ApplicationScope.MeshtasticWindow( { overlay, nodePositions, onMappableCountChanged, modifier -> DesktopTracerouteMap(overlay, nodePositions, onMappableCountChanged, modifier) }, + // Clear of the MapLibre logo and attribution row along the bottom edge. + LocalTracerouteMapOverlayInsetsProvider provides + TracerouteMapOverlayInsets(overlayPadding = PaddingValues(bottom = 48.dp)), ) { AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen(uiViewModel, multiBackstack) } } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt index 08c637b55e..35c2e68bd6 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/UpdateChecker.kt @@ -23,6 +23,7 @@ import io.ktor.client.statement.bodyAsText import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import org.meshtastic.core.common.util.safeCatching /** * Checks GitHub Releases for a newer published desktop build. @@ -36,7 +37,7 @@ class UpdateChecker(private val httpClient: HttpClient) { data class UpdateInfo(val versionName: String, val releaseUrl: String) /** Returns the latest published release when it is newer than [currentVersionName], null otherwise. */ - suspend fun check(currentVersionName: String): UpdateInfo? = runCatching { + suspend fun check(currentVersionName: String): UpdateInfo? = safeCatching { val release = Json.parseToJsonElement(httpClient.get(LATEST_RELEASE_URL).bodyAsText()).jsonObject val tag = release["tag_name"]?.jsonPrimitive?.content if (tag != null && isNewer(latest = tag, current = currentVersionName)) { diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopHttpCache.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopHttpCache.kt new file mode 100644 index 0000000000..e9448e6cb7 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopHttpCache.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.desktop.di + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.cache.storage.FileStorage +import kotlinx.io.files.Path +import org.meshtastic.core.database.desktopDataDir +import java.io.File + +/** + * What the desktop HTTP cache is trimmed to at launch. It is not enforced during a session, which only adds the handful + * of api.meshtastic.org JSON resources. + */ +internal const val HTTP_CACHE_MAX_BYTES = 10L * 1024L * 1024L + +/** The desktop HTTP cache directory, trimmed to [HTTP_CACHE_MAX_BYTES] at launch and created if missing. */ +internal fun preparedHttpCacheDir(): File = + File(desktopDataDir(), "http_cache").also { trimDirectoryToBudget(it, HTTP_CACHE_MAX_BYTES) } + +/** + * A copy of this client that caches responses under [cacheDir], for the small api.meshtastic.org JSON resources only. + * Ktor's `FileStorage` also keeps every stored response in memory for the client's lifetime, so the shared client, + * which downloads firmware, map layers and images, must stay uncached. + */ +internal fun HttpClient.withApiCache(cacheDir: File): HttpClient = config { + install(HttpCache) { publicStorage(FileStorage(Path(cacheDir.path))) } +} + +/** + * Keeps the most recently written files in [directory] that fit in [maxBytes] and deletes the rest. Ktor's file cache + * storage has no size bound of its own, and it serves stored responses from memory once loaded, so trimming happens + * once, before the client starts using the directory. + */ +internal fun trimDirectoryToBudget(directory: File, maxBytes: Long) { + directory.mkdirs() + var kept = 0L + directory + .walkTopDown() + .filter { it.isFile } + .sortedByDescending { it.lastModified() } + .forEach { file -> + val size = file.length() + if (kept + size <= maxBytes) kept += size else file.delete() + } +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt index 4d5e85f990..7d143a67a8 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt @@ -26,20 +26,16 @@ import okio.Path.Companion.toPath import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.core.database.desktopDataDir -import org.meshtastic.core.datastore.di.CoreChannelSetDataStore import org.meshtastic.core.datastore.di.CoreLocalConfigDataStore import org.meshtastic.core.datastore.di.CoreLocalStatsDataStore import org.meshtastic.core.datastore.di.CoreModuleConfigDataStore import org.meshtastic.core.datastore.di.DataStoreScope -import org.meshtastic.core.datastore.di.asCoreChannelSetDataStore import org.meshtastic.core.datastore.di.asCoreLocalConfigDataStore import org.meshtastic.core.datastore.di.asCoreLocalStatsDataStore import org.meshtastic.core.datastore.di.asCoreModuleConfigDataStore -import org.meshtastic.core.datastore.serializer.ChannelSetSerializer import org.meshtastic.core.datastore.serializer.LocalConfigSerializer import org.meshtastic.core.datastore.serializer.LocalStatsSerializer import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer -import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.LocalStats @@ -58,11 +54,6 @@ class DesktopProtoDataStoreModule { protoStore(ModuleConfigSerializer, "module_config.pb", { LocalModuleConfig.Builder().build() }, scope) .asCoreModuleConfigDataStore() - @Single - fun channelSetDataStore(scope: DataStoreScope): CoreChannelSetDataStore = - protoStore(ChannelSetSerializer, "channel_set.pb", { ChannelSet.Builder().build() }, scope) - .asCoreChannelSetDataStore() - @Single fun localStatsDataStore(scope: DataStoreScope): CoreLocalStatsDataStore = protoStore(LocalStatsSerializer, "local_stats.pb", { LocalStats.Builder().build() }, scope) diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt index 265f7e4d96..953656b85e 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt @@ -33,8 +33,6 @@ import org.koin.core.annotation.Single import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.ble.BluetoothRepository -import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.common.di.ServiceScope import org.meshtastic.core.data.datasource.BundledAssetReader import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.network.HttpClientDefaults @@ -42,34 +40,18 @@ import org.meshtastic.core.network.KermitHttpLogger import org.meshtastic.core.network.configureDefaultRetry import org.meshtastic.core.network.service.ApiService import org.meshtastic.core.network.service.ApiServiceImpl -import org.meshtastic.core.repository.AdminController -import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.ConnectionStateProvider -import org.meshtastic.core.repository.MeshDataHandler -import org.meshtastic.core.repository.MeshLocationManager -import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshNotificationManager -import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.MessageQueue -import org.meshtastic.core.repository.MessagingController import org.meshtastic.core.repository.NeighborInfoResponseProvider -import org.meshtastic.core.repository.NodeController -import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NotificationManager import org.meshtastic.core.repository.NotificationPrefs import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.PlatformAnalytics -import org.meshtastic.core.repository.QueryController -import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioController -import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioTransportFactory import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.ServiceStateWriter import org.meshtastic.core.repository.TracerouteResponseProvider -import org.meshtastic.core.repository.UiPrefs -import org.meshtastic.core.service.RadioControllerImpl import org.meshtastic.core.service.ServiceRepositoryImpl import org.meshtastic.desktop.DesktopBuildConfig import org.meshtastic.desktop.DesktopNotificationManager @@ -115,53 +97,6 @@ class DesktopRuntimeModule { connectionFactory = connectionFactory, ) - @Suppress("LongParameterList") - @Single( - binds = - [ - RadioController::class, - AdminController::class, - MessagingController::class, - NodeController::class, - QueryController::class, - ], - ) - fun radioController( - serviceRepository: ServiceRepository, - nodeRepository: NodeRepository, - commandSender: CommandSender, - nodeManager: NodeManager, - radioInterfaceService: RadioInterfaceService, - locationManager: MeshLocationManager, - packetRepository: Lazy, - dataHandler: Lazy, - analytics: PlatformAnalytics, - meshPrefs: MeshPrefs, - uiPrefs: UiPrefs, - databaseManager: DatabaseManager, - notificationManager: NotificationManager, - messageProcessor: Lazy, - radioConfigRepository: RadioConfigRepository, - scope: ServiceScope, - ): RadioController = RadioControllerImpl( - serviceRepository = serviceRepository, - nodeRepository = nodeRepository, - commandSender = commandSender, - nodeManager = nodeManager, - radioInterfaceService = radioInterfaceService, - locationManager = locationManager, - packetRepository = packetRepository, - dataHandler = dataHandler, - analytics = analytics, - meshPrefs = meshPrefs, - uiPrefs = uiPrefs, - databaseManager = databaseManager, - notificationManager = notificationManager, - messageProcessor = messageProcessor, - radioConfigRepository = radioConfigRepository, - scope = scope, - ) - /** * Only the Linux sender holds a native handle; the others are stateless. `Main.kt` closes it explicitly during * shutdown, because annotations have no `onClose` equivalent. @@ -194,8 +129,9 @@ class DesktopRuntimeModule { dispatchers = dispatchers, ) - /** Desktop uses the real `ApiService` implementation over the JVM `HttpClient` below — no flavor stub needed. */ - @Single fun apiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl + /** The real `ApiService` over its own disk-cached copy of the shared client below; see [withApiCache]. */ + @Single + fun apiService(httpClient: HttpClient): ApiService = ApiServiceImpl(httpClient.withApiCache(preparedHttpCacheDir())) /** Ktor [HttpClient] for JVM/Desktop — the equivalent of `CoreNetworkAndroidModule`'s OkHttp-backed client. */ @Single diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt index c15033acc6..6882a7da06 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt @@ -18,7 +18,6 @@ package org.meshtastic.desktop.di import org.koin.core.annotation.Module import org.koin.core.annotation.Single -import org.meshtastic.core.network.repository.MQTTRepository import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.MeshLocationManager @@ -27,7 +26,6 @@ import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.desktop.stub.NoopAppWidgetUpdater import org.meshtastic.desktop.stub.NoopCompassHeadingProvider import org.meshtastic.desktop.stub.NoopLocationRepository -import org.meshtastic.desktop.stub.NoopMQTTRepository import org.meshtastic.desktop.stub.NoopMagneticFieldProvider import org.meshtastic.desktop.stub.NoopMeshLocationManager import org.meshtastic.desktop.stub.NoopMeshWorkerManager @@ -39,8 +37,7 @@ import org.meshtastic.feature.node.compass.PhoneLocationProvider /** * Stubs for interfaces whose only real implementation needs Android APIs — WorkManager, widgets, location, sensors and - * analytics. [MQTTRepository] is the exception: it has a working `commonMain` implementation, and this binding - * deliberately shadows it because desktop does not run the MQTT bridge. + * analytics. */ @Module class DesktopStubsModule { @@ -55,8 +52,6 @@ class DesktopStubsModule { @Single fun locationRepository(): LocationRepository = NoopLocationRepository() - @Single fun mqttRepository(): MQTTRepository = NoopMQTTRepository() - @Single fun compassHeadingProvider(): CompassHeadingProvider = NoopCompassHeadingProvider() @Single fun phoneLocationProvider(): PhoneLocationProvider = NoopPhoneLocationProvider() diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopMapViewProvider.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopMapViewProvider.kt index 13088ff300..2487fc2710 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopMapViewProvider.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopMapViewProvider.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.desktop.map -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.koin.compose.koinInject import org.meshtastic.feature.map.layers.MapLayersManager import org.meshtastic.feature.map.maplibre.MapLibreMapViewProvider @@ -32,7 +32,7 @@ import org.meshtastic.feature.map.maplibre.layers.rememberRenderableLayers internal fun desktopMapViewProvider(): MapLibreMapViewProvider = MapLibreMapViewProvider( customLayers = { val layersManager: MapLayersManager = koinInject() - val importedLayers by layersManager.mapLayers.collectAsState() + val importedLayers by layersManager.mapLayers.collectAsStateWithLifecycle() rememberRenderableLayers(layersManager, importedLayers.filter { it.isVisible }) }, sitePlanner = { session -> DesktopSitePlannerSlot(session) }, diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt index a111c43a8f..f241f1e607 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt @@ -16,50 +16,37 @@ */ package org.meshtastic.desktop.notification -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.FirmwareUpdateNotice +import org.meshtastic.core.model.MeshBeaconOffer import org.meshtastic.core.model.Node import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.Notification import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.repository.notificationId import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.desktop_notification_title -import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.firmware_update_available +import org.meshtastic.core.resources.firmware_update_notification_android +import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.low_battery_message import org.meshtastic.core.resources.low_battery_title -import org.meshtastic.core.resources.new_node_seen +import org.meshtastic.core.resources.mesh_beacon_notification_body +import org.meshtastic.core.resources.mesh_beacon_notification_title import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry /** - * Desktop implementation of [MeshNotificationManager]. + * Desktop implementation of [MeshNotificationManager]: turns each mesh event into a [Notification] record and hands it + * to [NotificationManager], which gates it on the user's notification preferences and shows it through the OS. * - * Converts mesh-layer notification events into domain [Notification] objects and dispatches them through - * [NotificationManager], which ultimately surfaces them as Compose Desktop tray notifications. - * - * Android-only concepts (notification channels, foreground-service state updates) are intentionally no-ops. + * Android-only concepts (notification channels, the foreground-service notification, updating a posted notification in + * place) are no-ops here. * * Registered manually in `DesktopRuntimeModule` -- do **not** add `@Single` to avoid double-registration with the * `@ComponentScan("org.meshtastic.desktop")` in [DesktopDiModule][org.meshtastic.desktop.di.DesktopDiModule]. */ @Suppress("TooManyFunctions") -class DesktopMeshNotificationManager( - private val notificationManager: NotificationManager, - // Bridges the non-suspend MeshNotificationManager entry points to the suspending NotificationManager.dispatch. - // Injectable so tests can substitute a TestScope / TestDispatcher. - @Suppress("InjectDispatcher") private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), -) : MeshNotificationManager { - - /** - * Launches [build] on [scope] and dispatches the resulting [Notification], bridging the non-suspend entry points to - * the suspending [NotificationManager.dispatch]. [build] runs inside the coroutine so it may call suspend resource - * getters (e.g. [getString]). - */ - private fun dispatchAsync(build: suspend () -> Notification) = - scope.launch { notificationManager.dispatch(build()) } +class DesktopMeshNotificationManager(private val notificationManager: NotificationManager) : MeshNotificationManager { override fun clearNotifications() { notificationManager.cancelAll() @@ -86,7 +73,6 @@ class DesktopMeshNotificationManager( title = name, message = message, category = Notification.Category.Message, - contactKey = contactKey, isSilent = isSilent, id = contactKey.hashCode(), ), @@ -100,15 +86,7 @@ class DesktopMeshNotificationManager( waypointId: Int, isSilent: Boolean, ) { - notificationManager.dispatch( - Notification( - title = name, - message = message, - category = Notification.Category.Message, - contactKey = contactKey, - isSilent = isSilent, - ), - ) + notificationManager.dispatch(Notification(title = name, message = message, isSilent = isSilent)) } override suspend fun updateReactionNotification( @@ -119,64 +97,100 @@ class DesktopMeshNotificationManager( channelName: String?, isSilent: Boolean, ) { + notificationManager.dispatch( + Notification(title = name, message = emoji, category = Notification.Category.Message, isSilent = isSilent), + ) + } + + override suspend fun showAlertNotification(contactKey: String, name: String, alert: String) { + notificationManager.dispatch( + Notification(title = name, message = alert, category = Notification.Category.Alert), + ) + } + + override suspend fun showMeshBeaconNotification(offer: MeshBeaconOffer) { notificationManager.dispatch( Notification( - title = name, - message = emoji, - category = Notification.Category.Message, - contactKey = contactKey, - isSilent = isSilent, + title = getStringSuspend(Res.string.mesh_beacon_notification_title), + message = offer.message.ifBlank { getStringSuspend(Res.string.mesh_beacon_notification_body) }, + category = Notification.Category.MeshBeacon, ), ) } - override fun showAlertNotification(contactKey: String, name: String, alert: String) { - dispatchAsync { - Notification(title = name, message = alert, category = Notification.Category.Alert, contactKey = contactKey) - } - } - - override fun showNewNodeSeenNotification(node: Node) { - dispatchAsync { + override suspend fun showNewNodeSeenNotification(node: Node, title: String) { + notificationManager.dispatch( Notification( - title = getString(Res.string.new_node_seen, node.user.short_name), + title = title, message = node.user.long_name, category = Notification.Category.NodeEvent, - ) - } + id = node.num, + ), + ) } - override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) { - dispatchAsync { + override fun cancelNewNodeNotification(nodeNum: Int) { + notificationManager.cancel(nodeNum) + } + + override suspend fun showLowBatteryNotification(node: Node, isRemote: Boolean) { + notificationManager.dispatch( Notification( - title = getString(Res.string.low_battery_title, node.user.short_name), - message = getString(Res.string.low_battery_message, node.user.long_name, node.batteryLevel ?: 0), + title = getStringSuspend(Res.string.low_battery_title, node.user.short_name), + message = getStringSuspend(Res.string.low_battery_message, node.user.long_name, node.batteryLevel ?: 0), category = Notification.Category.Battery, id = node.num, - ) - } + ), + ) } - override fun showClientNotification(clientNotification: ClientNotification) { - dispatchAsync { - Notification( - title = getString(Res.string.desktop_notification_title), - message = clientNotification.message, - category = Notification.Category.Alert, - id = clientNotification.toString().hashCode(), - ) - } - } - - override suspend fun cancelMessageNotification(contactKey: String) { - notificationManager.cancel(contactKey.hashCode()) + override suspend fun updateLowBatteryNotification(node: Node, isRemote: Boolean) { + // No-op: an OS notification cannot be refreshed in place, and re-posting would alert again. } override fun cancelLowBatteryNotification(node: Node) { notificationManager.cancel(node.num) } - override fun clearClientNotification(notification: ClientNotification) { - notificationManager.cancel(notification.toString().hashCode()) + override suspend fun showClientNotification( + clientNotification: ClientNotification, + title: String, + severity: Notification.Type, + ) { + notificationManager.dispatch( + Notification( + title = title, + message = clientNotification.message, + type = severity, + category = Notification.Category.Client, + id = clientNotification.notificationId(), + ), + ) + } + + override fun clearClientNotification(clientNotification: ClientNotification) { + notificationManager.cancel(clientNotification.notificationId()) + } + + override suspend fun showFirmwareUpdateNotification(notice: FirmwareUpdateNotice): Boolean = + notificationManager.dispatch( + Notification( + title = getStringSuspend(Res.string.firmware_update_available), + message = + getStringSuspend( + Res.string.firmware_update_notification_android, + notice.currentVersion, + notice.stableVersion, + ), + category = Notification.Category.Service, + id = notice.notificationKey.hashCode(), + ), + ) + + // The reconnect-blocked notice is about an Android runtime permission; desktop never raises it. + override suspend fun showReconnectBlockedNotification(title: String, message: String): Boolean = false + + override suspend fun cancelMessageNotification(contactKey: String) { + notificationManager.cancel(contactKey.hashCode()) } } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt index 9afdd8b3c3..fb93bb0392 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt @@ -200,10 +200,17 @@ class LinuxNotificationSender( val category = when (notification.category) { Notification.Category.Message -> "im.received" + Notification.Category.Battery -> "device.warning" - Notification.Category.Alert -> "device.error" + + Notification.Category.Alert, + Notification.Category.Client, + -> "device.error" + Notification.Category.NodeEvent -> "network" + Notification.Category.MeshBeacon -> "network" + Notification.Category.Service -> "device" } libnotify.notify_notification_set_category(ptr, category) diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt index d614e717a3..203f7d999b 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt @@ -21,7 +21,7 @@ import com.sun.jna.Function import com.sun.jna.NativeLibrary import com.sun.jna.Pointer import org.meshtastic.core.repository.Notification -import java.util.UUID +import kotlin.uuid.Uuid /** * Sends notifications through macOS UserNotifications (`UNUserNotificationCenter`) via JNA + Objective-C runtime. This @@ -60,10 +60,17 @@ class MacOSNotificationSender private constructor(private val bridge: MacNotific internal fun categorySubtitle(category: Notification.Category): String = when (category) { Notification.Category.Message -> "Message" + Notification.Category.NodeEvent -> "Node Event" + Notification.Category.MeshBeacon -> "Mesh Invitation" + Notification.Category.Battery -> "Low Battery" - Notification.Category.Alert -> "Alert" + + Notification.Category.Alert, + Notification.Category.Client, + -> "Alert" + Notification.Category.Service -> "Service" } @@ -195,7 +202,7 @@ private class JnaMacNotificationBridge : MacNotificationBridge { msg( requestClass, selector("requestWithIdentifier:content:trigger:"), - nsString(UUID.randomUUID().toString()) ?: return false, + nsString(Uuid.random().toString()) ?: return false, content, Pointer.NULL, ) ?: return false diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt index b028b59c05..24524390fe 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt @@ -42,6 +42,8 @@ class DesktopMessageQueue( ) : MessageQueue { private val scope = CoroutineScope(SupervisorJob() + dispatchers.io) + // A failed or cancelled send rolls its claim back (NonCancellable in the repository); cancellation is rethrown. + @Suppress("SuspendFunSwallowedCancellation") override suspend fun enqueue(persistedId: PersistedPacketId) { scope.launch { if (persistedId.uuid <= 0L) return@launch diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt index efa79d1d9c..b6b303338e 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt @@ -24,12 +24,15 @@ import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceType import org.meshtastic.core.model.InterfaceId +import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.network.SerialTransport import org.meshtastic.core.network.radio.BaseRadioTransportFactory +import org.meshtastic.core.network.radio.MockRadioTransport import org.meshtastic.core.network.radio.TcpRadioTransport import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioTransport import org.meshtastic.core.repository.RadioTransportFactory +import org.meshtastic.desktop.DesktopBuildConfig /** * Desktop implementation of [RadioTransportFactory] delegating multiplatform transports (BLE, TCP) and providing @@ -47,13 +50,22 @@ class DesktopRadioTransportFactory( override val supportedDeviceTypes: List = listOf(DeviceType.TCP, DeviceType.BLE, DeviceType.USB) - // Desktop has no unlock gesture and no demo entry in its picker; the virtual transports stay inadmissible. - override val mockTransportEnabled: StateFlow = MutableStateFlow(false) + // Desktop has no unlock gesture, so Demo Mode is a debug-build feature: it admits the `m` addresses, including + // the hidden showcase, and offers the demo entry in the picker. + override val mockTransportEnabled: StateFlow = MutableStateFlow(DesktopBuildConfig.IS_DEBUG) /** Desktop bundles no capture asset, and [createPlatformTransport] does not wire a replay address. */ override val isReplayTransportAvailable: Boolean = false override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport = when { + address.startsWith(InterfaceId.MOCK.id) -> { + MockRadioTransport( + callback = service, + scope = service.serviceScope, + address = address.removePrefix(InterfaceId.MOCK.id.toString()), + ) + } + address.startsWith(InterfaceId.TCP.id) -> { TcpRadioTransport( callback = service, @@ -72,6 +84,6 @@ class DesktopRadioTransportFactory( ) } - else -> error("Unsupported transport for address: $address") + else -> error("Unsupported transport for address: ${address.anonymize()}") } } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt index cd69ea5c38..2d8827d937 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt @@ -31,7 +31,6 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MeshActivity -import org.meshtastic.core.network.repository.MQTTRepository import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.DataPair import org.meshtastic.core.repository.Location @@ -45,8 +44,6 @@ import org.meshtastic.core.repository.RadioSessionContext import org.meshtastic.core.repository.RadioSessionLease import org.meshtastic.core.repository.ReceivedRadioFrame import org.meshtastic.core.repository.TransportDisconnectReason -import org.meshtastic.proto.MqttClientProxyMessage -import org.meshtastic.mqtt.ConnectionState as MqttConnectionState import org.meshtastic.proto.Position as ProtoPosition /** @@ -172,17 +169,3 @@ class NoopLocationRepository : LocationRepository { } // endregion - -// region Network Stubs (MQTT — not yet available on Desktop) - -class NoopMQTTRepository : MQTTRepository { - override fun disconnect() {} - - override val proxyMessageFlow: Flow = emptyFlow() - - override fun publish(topic: String, data: ByteArray, retained: Boolean) {} - - override val connectionState = MutableStateFlow(MqttConnectionState.Disconnected.Idle) -} - -// endregion diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/DesktopLoggingTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/DesktopLoggingTest.kt new file mode 100644 index 0000000000..9827ae2000 --- /dev/null +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/DesktopLoggingTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.desktop + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.platformLogWriter +import org.meshtastic.core.common.log.InMemoryLogBuffer +import java.util.UUID +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DesktopLoggingTest { + + @AfterTest + fun tearDown() { + Logger.setLogWriters(platformLogWriter()) + Logger.setMinSeverity(Severity.Verbose) + } + + private fun marker(level: String) = "desktop-logging-test-$level-${UUID.randomUUID()}" + + @Test + fun `release build keeps Verbose and Debug lines out of the exportable buffer`() { + installDesktopLogging(isDebug = false) + val verbose = marker("verbose") + val debug = marker("debug") + val info = marker("info") + + Logger.v { verbose } + Logger.d { debug } + Logger.i { info } + + val buffer = InMemoryLogBuffer.snapshot() + assertFalse(verbose in buffer, "Verbose line reached the release buffer") + assertFalse(debug in buffer, "Debug line reached the release buffer") + assertTrue(info in buffer, "Info line is missing from the release buffer") + } + + @Test + fun `debug build keeps every level in the exportable buffer`() { + installDesktopLogging(isDebug = true) + val verbose = marker("verbose") + val debug = marker("debug") + + Logger.v { verbose } + Logger.d { debug } + + val buffer = InMemoryLogBuffer.snapshot() + assertTrue(verbose in buffer, "Verbose line is missing from the debug buffer") + assertTrue(debug in buffer, "Debug line is missing from the debug buffer") + } +} diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopHttpCacheTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopHttpCacheTest.kt new file mode 100644 index 0000000000..d3f0e20b93 --- /dev/null +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopHttpCacheTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.desktop.di + +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.pluginOrNull +import kotlinx.serialization.json.Json +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class DesktopHttpCacheTest { + + private val dir: File = Files.createTempDirectory("http-cache-test").toFile() + + @AfterTest + fun cleanUp() { + dir.deleteRecursively() + } + + private fun entry(name: String, bytes: Int, modifiedAt: Long) = File(dir, name).apply { + writeBytes(ByteArray(bytes)) + setLastModified(modifiedAt) + } + + @Test + fun `the oldest entries go first once the cache is over budget`() { + entry("oldest", bytes = 400, modifiedAt = 1_000_000L) + entry("middle", bytes = 400, modifiedAt = 2_000_000L) + entry("newest", bytes = 400, modifiedAt = 3_000_000L) + + trimDirectoryToBudget(dir, maxBytes = 1_000) + + assertEquals(setOf("middle", "newest"), dir.list().orEmpty().toSet()) + } + + @Test + fun `a cache within budget is left alone`() { + entry("a", bytes = 100, modifiedAt = 1_000_000L) + entry("b", bytes = 100, modifiedAt = 2_000_000L) + + trimDirectoryToBudget(dir, maxBytes = 1_000) + + assertEquals(setOf("a", "b"), dir.list().orEmpty().toSet()) + } + + @Test + fun `only the api client caches responses`() { + val shared = DesktopRuntimeModule().httpClient(Json) + val api = shared.withApiCache(File(dir, "http_cache")) + try { + assertNull(shared.pluginOrNull(HttpCache), "the shared client also carries firmware and images") + assertNotNull(api.pluginOrNull(HttpCache)) + } finally { + api.close() + shared.close() + } + } + + @Test + fun `a missing cache directory is created`() { + val missing = File(dir, "nested/http_cache") + + trimDirectoryToBudget(missing, maxBytes = 1_000) + + assertTrue(missing.isDirectory) + } +} diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt index 9ab331e0aa..3e6e5a6e1e 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt @@ -26,8 +26,6 @@ import org.koin.plugin.module.dsl.koinApplication import org.koin.test.verify.verify import org.meshtastic.core.ble.BleLogFormat import org.meshtastic.core.ble.BleLogLevel -import org.meshtastic.core.network.repository.MQTTRepository -import org.meshtastic.desktop.stub.NoopMQTTRepository import org.meshtastic.feature.docs.translation.DocTranslationService import org.meshtastic.feature.docs.translation.NoOpDocTranslator import org.meshtastic.feature.messaging.translation.MessageTranslationService @@ -67,12 +65,10 @@ class DesktopKoinTest { @Test fun `desktop bindings win over the shared graph`() { // @Configuration modules load before the ones listed in @KoinApplication, and Koin is last-wins, so which - // binding survives is ordering-dependent. MQTTRepository is the live case: core:network commonMain declares - // MQTTRepositoryImpl, and desktop must shadow it. verify() only checks definitions exist, never who won. + // binding survives is ordering-dependent. verify() only checks definitions exist, never who won. val app = koinApplication() try { val koin = app.koin - assertIs(koin.get()) assertIs(koin.get()) assertIs(koin.get()) } finally { diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManagerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManagerTest.kt index 139df5971e..713ea07220 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManagerTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManagerTest.kt @@ -16,44 +16,83 @@ */ package org.meshtastic.desktop.notification -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.meshtastic.core.model.Node import org.meshtastic.core.repository.Notification import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.repository.notificationId +import org.meshtastic.proto.ClientNotification import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) class DesktopMeshNotificationManagerTest { - /** Records everything dispatched so the async bridge can be asserted deterministically. */ - private class FakeNotificationManager : NotificationManager { + private class FakeNotificationManager(var accepts: Boolean = true) : NotificationManager { val dispatched = mutableListOf() + val cancelled = mutableListOf() override suspend fun dispatch(notification: Notification): Boolean { - dispatched.add(notification) - return true + if (accepts) dispatched.add(notification) + return accepts } - override fun cancel(id: Int) {} + override fun cancel(id: Int) { + cancelled.add(id) + } override fun cancelAll() {} } - @Test - fun `showAlertNotification dispatches on the injected scope`() = runTest(UnconfinedTestDispatcher()) { - val notificationManager = FakeNotificationManager() - // backgroundScope inherits the UnconfinedTestDispatcher, so the launched dispatch runs eagerly and is - // observable synchronously — no virtual-time advance or manual teardown needed. - val manager = DesktopMeshNotificationManager(notificationManager, scope = backgroundScope) + private val notificationManager = FakeNotificationManager() + private val manager = DesktopMeshNotificationManager(notificationManager) + @Test + fun `critical alerts dispatch in the alert category`() = runTest { manager.showAlertNotification(contactKey = "contact-1", name = "Alert", alert = "Something happened") val dispatched = notificationManager.dispatched.single() assertEquals("Alert", dispatched.title) assertEquals("Something happened", dispatched.message) assertEquals(Notification.Category.Alert, dispatched.category) - assertEquals("contact-1", dispatched.contactKey) + } + + @Test + fun `client notifications keep their title and severity under a stable id`() = runTest { + val clientNotification = ClientNotification.Builder().also { wb -> wb.message = "Duplicate key" }.build() + + manager.showClientNotification(clientNotification, title = "Key conflict", severity = Notification.Type.Warning) + manager.clearClientNotification(clientNotification) + + val dispatched = notificationManager.dispatched.single() + assertEquals("Key conflict", dispatched.title) + assertEquals(Notification.Type.Warning, dispatched.type) + assertEquals(Notification.Category.Client, dispatched.category) + assertEquals(clientNotification.notificationId(), dispatched.id) + assertEquals(listOf(clientNotification.notificationId()), notificationManager.cancelled) + } + + @Test + fun `a low-battery refresh never re-posts`() = runTest { + manager.updateLowBatteryNotification(Node(num = 7), isRemote = false) + + assertTrue(notificationManager.dispatched.isEmpty()) + } + + @Test + fun `new-node notifications cancel by node number`() = runTest { + manager.showNewNodeSeenNotification(Node(num = 7), title = "New node seen: N7") + manager.cancelNewNodeNotification(7) + + assertEquals(7, notificationManager.dispatched.single().id) + assertEquals("New node seen: N7", notificationManager.dispatched.single().title) + assertEquals(listOf(7), notificationManager.cancelled) + } + + @Test + fun `reconnect-blocked is never shown on desktop`() = runTest { + assertFalse(manager.showReconnectBlockedNotification("title", "message")) + assertTrue(notificationManager.dispatched.isEmpty()) } } diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt index ea2447c45b..ae924e77d8 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt @@ -94,7 +94,7 @@ class DesktopNotificationManagerTest { assertTrue(dispatched, "Tray fallback acceptance should count as delivery-accepted") assertNotNull(fallback, "Expected fallback notification to be emitted") - assertEquals("Fallback", fallback!!.title) + assertEquals("Fallback", fallback.title) } @Test diff --git a/docs-screenshots/src/screenshotTest/kotlin/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTests.kt b/docs-screenshots/src/screenshotTest/kotlin/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTests.kt index 724a4190b6..d109b25951 100644 --- a/docs-screenshots/src/screenshotTest/kotlin/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTests.kt +++ b/docs-screenshots/src/screenshotTest/kotlin/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTests.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark import com.android.tools.screenshot.PreviewTest import org.meshtastic.feature.connections.component.BluetoothScanPreview -import org.meshtastic.feature.connections.component.EmptyStateContentPreview // Doc-framed connections compositions (bounded-height crops tuned for the docs site). The atomic connections // components (device list item, transport selector, disconnect button, etc.) remain regression-gated in @@ -32,10 +31,3 @@ import org.meshtastic.feature.connections.component.EmptyStateContentPreview fun ScreenshotConnectionsBluetoothScan() { BluetoothScanPreview() } - -@PreviewTest -@PreviewLightDark -@Composable -fun ScreenshotEmptyStateContent() { - EmptyStateContentPreview() -} diff --git a/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png b/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png deleted file mode 100644 index 36e232abe5..0000000000 Binary files a/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png and /dev/null differ diff --git a/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png b/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png deleted file mode 100644 index a6584d606e..0000000000 Binary files a/docs-screenshots/src/screenshotTestDebug/reference/org/meshtastic/screenshots/docs/feature/ConnectionsDocScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png and /dev/null differ diff --git a/docs/Gemfile b/docs/Gemfile index 9248b3181d..8a8178033b 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -3,6 +3,5 @@ source "https://rubygems.org" gem "jekyll", "~> 4.3" gem "just-the-docs" gem "jekyll-redirect-from" -gem "jekyll-remote-theme" gem "csv" gem "webrick" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000000..6f3b9556f5 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,254 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + bigdecimal (4.1.3) + colorator (1.1.0) + concurrent-ruby (1.3.8) + csv (3.3.6) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + forwardable-extended (2.6.0) + google-protobuf (4.36.2) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-x86-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-x86-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + http_parser.rb (0.8.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + jekyll (4.4.1) + addressable (~> 2.4) + base64 (~> 0.2) + colorator (~> 1.0) + csv (~> 3.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + json (~> 2.6) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.3, >= 0.3.6) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-include-cache (0.2.2) + jekyll (>= 3.7, < 5.0) + jekyll-redirect-from (0.17.0) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) + jekyll-seo-tag (2.9.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + json (2.21.2) + just-the-docs (0.12.0) + jekyll (>= 3.8.5) + jekyll-include-cache + jekyll-seo-tag (>= 2.0) + rake (>= 12.3.1) + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (7.0.5) + rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.4) + rouge (4.7.0) + safe_yaml (1.0.5) + sass-embedded (1.105.1) + google-protobuf (~> 4.31) + rake (~> 13.3) + sass-embedded (1.105.1-aarch64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-aarch64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-aarch64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-arm-linux-androideabi) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-arm-linux-gnueabihf) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-arm-linux-musleabihf) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-riscv64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-riscv64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-riscv64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-x86_64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-x86_64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.105.1-x86_64-linux-musl) + google-protobuf (~> 4.31) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.2) + +PLATFORMS + aarch64-linux + aarch64-linux-android + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-androideabi + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin + riscv64-linux-android + riscv64-linux-gnu + riscv64-linux-musl + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-android + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + csv + jekyll (~> 4.3) + jekyll-redirect-from + just-the-docs + webrick + +CHECKSUMS + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 + bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae + colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38 + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 + em-websocket (0.5.3) sha256=f56a92bde4e6cb879256d58ee31f124181f68f8887bd14d53d5d9a292758c6a8 + eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + forwardable-extended (2.6.0) sha256=1bec948c469bbddfadeb3bd90eb8c85f6e627a412a3e852acfd7eaedbac3ec97 + google-protobuf (4.36.2) sha256=2f8daff50ae14aad36ab0ccdcf284065fa71d726c444efe114e6db82ffe62494 + google-protobuf (4.36.2-aarch64-linux-gnu) sha256=c3279ec6eaa59f6ed6276eda2a33cffb4306471183ab79a332e698b8564f5169 + google-protobuf (4.36.2-aarch64-linux-musl) sha256=0772c9dde1e6dce9a6b44d1d35c28c29adee99d9bd6aff5e21840daeb22161ce + google-protobuf (4.36.2-arm64-darwin) sha256=f70d810e6bbab9d8ad9c1d5fd685a21de4c10ad3ad32efb9455e17e5b1286917 + google-protobuf (4.36.2-x86-linux-gnu) sha256=ffcd06d49bffca24afff18c900dd61a51ea3028d43c47d0d9621d00bf38a5866 + google-protobuf (4.36.2-x86-linux-musl) sha256=e1afec03babd73c9117cc09d5fc2a98dfef37a414bfd8a4d0ceaac7bd66fc028 + google-protobuf (4.36.2-x86_64-darwin) sha256=8d1bd2a344f7f589703b202ad2ee4cca8ef731b4ced45782ef92d385967384ba + google-protobuf (4.36.2-x86_64-linux-gnu) sha256=d25a820873e423f1d209ae8c672a4a28b019e2429debd0901d26670a0b4e14db + google-protobuf (4.36.2-x86_64-linux-musl) sha256=0fa17686c896da96a60a705e56ddc7d4ff56413f52787c655bd4c9fb33ffe14f + http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + jekyll (4.4.1) sha256=4c1144d857a5b2b80d45b8cf5138289579a9f8136aadfa6dd684b31fe2bc18c1 + jekyll-include-cache (0.2.2) sha256=fe31c75187c37b035fb3b62b689f1ca284f1871c3bb7f607a8bdf3cc439b0aa1 + jekyll-redirect-from (0.17.0) sha256=703dab0428b20bb5d550ab83d3c28b65bb5ecc0f734c5e61ee680de391b3e897 + jekyll-sass-converter (3.1.0) sha256=83925d84f1d134410c11d0c6643b0093e82e3a3cf127e90757a85294a3862443 + jekyll-seo-tag (2.9.0) sha256=0260015a8e1df9bf195cdfb0c675b7b2883fd8cbf12556e1c1cbe36a831c6852 + jekyll-watch (2.2.1) sha256=bc44ed43f5e0a552836245a54dbff3ea7421ecc2856707e8a1ee203a8387a7e1 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + just-the-docs (0.12.0) sha256=15f2839ac9082898d60f33b978aa6f8e46fc50ba8fac20ae7a7f0e1fb295523e + kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa + kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729 + liquid (4.0.4) sha256=4fcfebb1a045e47918388dbb7a0925e7c3893e58d2bd6c3b3c73ec17a2d8fdb3 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mercenary (0.4.0) sha256=b25a1e4a59adca88665e08e24acf0af30da5b5d859f7d8f38fba52c28f405138 + pathutil (0.16.2) sha256=e43b74365631cab4f6d5e4228f812927efc9cb2c71e62976edcb252ee948d589 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739 + safe_yaml (1.0.5) sha256=a6ac2d64b7eb027bdeeca1851fe7e7af0d668e133e8a88066a0c6f7087d9f848 + sass-embedded (1.105.1) sha256=2025547ac561a16d0977d9b5da39455ce89648a902cfbbab7c35cc740a78e561 + sass-embedded (1.105.1-aarch64-linux-android) sha256=1b2e7541c7f0ad49720d921ac03d0d52a5e142bb6edc55905b0b916831b7830b + sass-embedded (1.105.1-aarch64-linux-gnu) sha256=e86b54a8dc1ce0d348cf3e963b5f5650995666db9fdb534c4d6febaeaddc5c6b + sass-embedded (1.105.1-aarch64-linux-musl) sha256=8f36f6de801adbaff3280f6758df790a4f97340ad10034df58cfda280c445c11 + sass-embedded (1.105.1-arm-linux-androideabi) sha256=845ef35a2a599f81f44a19782dce0593fdbcbda41e8dc8c46afbf069a7a4dcfd + sass-embedded (1.105.1-arm-linux-gnueabihf) sha256=8c134162e7d8769a01a3775aff66b14e97677f3eda025dc353b0f994ad50e058 + sass-embedded (1.105.1-arm-linux-musleabihf) sha256=7e1c8b19c1304d7930e9c95d066b63fe19c3e50aea301c70981dfa543e80b4bf + sass-embedded (1.105.1-arm64-darwin) sha256=e414773b18cd25851c292e34a2462eae04011fffc0eb94d5b65a40f29b1044ac + sass-embedded (1.105.1-riscv64-linux-android) sha256=37a66fea86e0eb24cc933a99a751bbd8d2bdb47d448fc1fc83ea40ba4a8682f4 + sass-embedded (1.105.1-riscv64-linux-gnu) sha256=a2341c247099304f5fd57a58a31eba459fee64ea62dbe75fb2c2928cd1c097aa + sass-embedded (1.105.1-riscv64-linux-musl) sha256=a6fb0a60eb243818066912e285ad25ec59ed0167be7ba745f4d85ea140c3f3b8 + sass-embedded (1.105.1-x86_64-darwin) sha256=8cd6e33e1511f76f3f65001998cba419cffbccdeb699c6abedb36fd24dc3d46f + sass-embedded (1.105.1-x86_64-linux-android) sha256=b4bb07305ac4171ab7c327b43f1a75d63d8b88b460416c195d9aab038f12c084 + sass-embedded (1.105.1-x86_64-linux-gnu) sha256=9038bc2256d7ecbfd352c15b77a3c8b6f0b72b763348d26564b5c0a123f48351 + sass-embedded (1.105.1-x86_64-linux-musl) sha256=be8897b9e2e27e9a6eac30e198ec7c9479b255f0a9da3109d4749e544ed75a11 + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 + +BUNDLED WITH + 4.0.20 diff --git a/docs/README.md b/docs/README.md index 9c5d65cb44..7342a5068d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,33 +57,39 @@ channels (GitHub Pages must be configured to serve from that branch): | Path | Content | Published by | |------|---------|--------------| -| `/` | Latest production release (default landing) | `docs-release.yml` on `vX.Y.Z` tags | -| `/vX.Y.Z/` | Permanent per-release copy | `docs-release.yml` on `vX.Y.Z` tags | -| `/vX.Y.Z-open.N/` | Per-tag open-testing snapshot | `docs-release.yml` on `vX.Y.Z-open.N` tags | -| `/vX.Y.Z-closed.N/` | Per-tag closed-testing snapshot | `docs-release.yml` on `vX.Y.Z-closed.N` tags | -| `/main/` | Snapshot of the `main` branch | `docs-deploy.yml` on pushes to `main` | -| `/api/` | Dokka API reference | `docs-deploy.yml`, plus production releases | +| `/` | Latest production release (default landing) | `docs-release.yml` on `vX.Y.Z` tags, dispatched by `promote.yml` | +| `/vX.Y.Z/` | Copy of the current and the previous production release | `docs-release.yml` on `vX.Y.Z` tags, dispatched by `promote.yml` | +| `/vX.Y.Z-open.N/` | Per-tag open-testing snapshot | `docs-release.yml` on `vX.Y.Z-open.N` tags, dispatched by `promote.yml` | +| `/vX.Y.Z-closed.N/` | Per-tag closed-testing snapshot | `docs-release.yml` on `vX.Y.Z-closed.N` tags, dispatched by `promote.yml` | +| `/main/` | Snapshot of the `main` branch | `docs-deploy.yml` on pushes to `main` that touch the site | +| `/api/` | Dokka API reference | `docs-deploy.yml` daily, plus production releases | | `/versions.json` | Version manifest for the site's version switcher | regenerated on every deploy | +`promote.yml` dispatches `docs-release.yml` after every open, closed and +production promotion, because the tag it creates with `GITHUB_TOKEN` starts no +workflow. The tag trigger of `docs-release.yml` covers a tag pushed by hand. + `-internal.N` tags are deliberately not published — they are cut many times per cycle and are not a documented channel. Prerelease snapshots accumulate during a version cycle so testers can read the docs for the exact build they are running. Once the production `vX.Y.Z` tag -ships, `/vX.Y.Z/` supersedes them and **Post-Release Cleanup** (run with -`base_version=X.Y.Z`) reaps the `vX.Y.Z-open.*` / `vX.Y.Z-closed.*` directories -along with the prerelease tags. That workflow defaults to a dry run. +ships, `/vX.Y.Z/` supersedes them, and once it is published Docs Release +dispatches **Post-Release Cleanup**, which reaps every open and closed directory +and prerelease tag at or below `X.Y.Z`. It also keeps two production copies, +`/vX.Y.Z/` and the newest one below it, and removes every older one. Copies above +`X.Y.Z` are left alone, so a backfill never removes newer docs. A manual dispatch +defaults to a dry run. Only production releases own `/` and rebuild `/api/`. Prerelease tags publish -their own directory only: `/api/` is unversioned and already refreshed by every -push to `main`, so rebuilding Dokka (~14 min) per prerelease tag would cost far -more than it refreshes. Until a production release exists, `/` redirects to the +their own directory only, since `/api/` is unversioned and `docs-deploy.yml` +rebuilds it from `main` every day. Until a production release exists, `/` redirects to the best available channel — newest open, then newest closed, then `/main/` — and upgrades automatically as better channels appear. Real release content at the root is never overwritten by that fallback. Each deploy overlays only its own channels via `scripts/docs/publish-to-gh-pages.sh`, -so release history accumulates instead of being wiped by the next deploy. The header +so every other channel survives the next deploy. The header version dropdown (`_includes/version_switcher.html`) reads `/versions.json` at runtime; a separate header link points to the upstream docs at meshtastic.org. To backfill a release (e.g. after first enabling this), run the "Docs Release" workflow manually diff --git a/docs/_config.yml b/docs/_config.yml index 39ddd8b25e..403c5cf22a 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -3,15 +3,11 @@ description: "User and developer documentation for the Meshtastic Android, Deskt baseurl: "" url: "" -# just-the-docs theme -# Local builds use `gem "just-the-docs"` from Gemfile. -# GitHub Pages uses remote_theme for hosted builds. +# just-the-docs theme, the gem version pinned in Gemfile.lock theme: just-the-docs -remote_theme: just-the-docs/just-the-docs@v0.11.0 # Plugins plugins: - - jekyll-remote-theme - jekyll-redirect-from # Navigation @@ -98,6 +94,12 @@ defaults: layout: locale_page locale: fi-rFI nav_exclude: true + - scope: + path: "fr-rCA" + values: + layout: locale_page + locale: fr-rCA + nav_exclude: true - scope: path: "fr-rFR" values: diff --git a/docs/_data/locales.yml b/docs/_data/locales.yml index e0f94ef44d..104b6f46c2 100644 --- a/docs/_data/locales.yml +++ b/docs/_data/locales.yml @@ -33,6 +33,9 @@ et-rEE: fi-rFI: name: "Suomi" dir: ltr +fr-rCA: + name: "Français (Canada)" + dir: ltr fr-rFR: name: "Français" dir: ltr diff --git a/docs/_includes/version_switcher.html b/docs/_includes/version_switcher.html index acb09f814c..c102d1912e 100644 --- a/docs/_includes/version_switcher.html +++ b/docs/_includes/version_switcher.html @@ -4,7 +4,7 @@ The site is deployed as parallel channels on gh-pages: / -> latest published release (default) /main/ -> snapshot of the main branch - /vX.Y.Z/ -> each published release + /vX.Y.Z/ -> the current and previous production release Which channel this build belongs to is inferred from site.baseurl (e.g. "/Meshtastic-Android", "/Meshtastic-Android/main", diff --git a/docs/ar-rSA/user/app-functions.md b/docs/ar-rSA/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ar-rSA/user/app-functions.md +++ b/docs/ar-rSA/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ar-rSA/user/connections.md b/docs/ar-rSA/user/connections.md index 05bd84c2f1..ef52fe003f 100644 --- a/docs/ar-rSA/user/connections.md +++ b/docs/ar-rSA/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| إعدادات بلوتوث | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ar-rSA/user/debug-logs.md b/docs/ar-rSA/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ar-rSA/user/debug-logs.md +++ b/docs/ar-rSA/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ar-rSA/user/desktop.md b/docs/ar-rSA/user/desktop.md index b33c76b28a..fca994623c 100644 --- a/docs/ar-rSA/user/desktop.md +++ b/docs/ar-rSA/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ar-rSA/user/discovery.md b/docs/ar-rSA/user/discovery.md index 2f0fc47232..f463191048 100644 --- a/docs/ar-rSA/user/discovery.md +++ b/docs/ar-rSA/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | الوصف | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | الوصف | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ar-rSA/user/firmware.md b/docs/ar-rSA/user/firmware.md index e6e581c256..bb38986431 100644 --- a/docs/ar-rSA/user/firmware.md +++ b/docs/ar-rSA/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ar-rSA/user/help-and-docs.md b/docs/ar-rSA/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ar-rSA/user/help-and-docs.md +++ b/docs/ar-rSA/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ar-rSA/user/map-and-waypoints.md b/docs/ar-rSA/user/map-and-waypoints.md index 3ddafd3094..c85c51f14b 100644 --- a/docs/ar-rSA/user/map-and-waypoints.md +++ b/docs/ar-rSA/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ar-rSA/user/messages-and-channels.md b/docs/ar-rSA/user/messages-and-channels.md index 33eb074c98..4ecaa911d2 100644 --- a/docs/ar-rSA/user/messages-and-channels.md +++ b/docs/ar-rSA/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ar-rSA/user/mqtt.md b/docs/ar-rSA/user/mqtt.md index bc22fad019..961a902907 100644 --- a/docs/ar-rSA/user/mqtt.md +++ b/docs/ar-rSA/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ar-rSA/user/node-metrics.md b/docs/ar-rSA/user/node-metrics.md index abe7cbfacf..1730816b1e 100644 --- a/docs/ar-rSA/user/node-metrics.md +++ b/docs/ar-rSA/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ar-rSA/user/nodes.md b/docs/ar-rSA/user/nodes.md index 6c7565363c..5c9d7048c9 100644 --- a/docs/ar-rSA/user/nodes.md +++ b/docs/ar-rSA/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| عربي | الوصف | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| عربي | الوصف | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | الوصف | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | آخر ظهور | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | المسافة | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ar-rSA/user/notifications.md b/docs/ar-rSA/user/notifications.md new file mode 100644 index 0000000000..978c21475a --- /dev/null +++ b/docs/ar-rSA/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| الرسائل | Direct message notifications | A message sent directly to you | The conversation | +| الرسائل | Broadcast message notifications | A message on one of your channels | The channel | +| الرسائل | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| الرسائل | Alert notifications | A critical alert from a node | The conversation | +| Mesh | إشعارات العقدة الجديدة | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| الجهاز | خدمة الإشعارات | The connection to your node while the app runs in the background | The app | +| الجهاز | إشعارات انخفاض شدة البطارية | Your node's battery running low | The node's details | +| الجهاز | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| الجهاز | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ar-rSA/user/onboarding.md b/docs/ar-rSA/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/ar-rSA/user/onboarding.md +++ b/docs/ar-rSA/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ar-rSA/user/settings-module-admin.md b/docs/ar-rSA/user/settings-module-admin.md index 94dd6d1980..8eb2b315be 100644 --- a/docs/ar-rSA/user/settings-module-admin.md +++ b/docs/ar-rSA/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## إعدادات الجهاز Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | الوصف | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | الوصف | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | الوصف | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | الوصف | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | استغرق وقت طويل | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | الوصف | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | الوصف | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | الوصف | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | الوصف | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | الوصف | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | الوصف | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | الوصف | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | الوصف | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | الوصف | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | الوصف | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | الحالي | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | الوصف | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | الوصف | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | الوصف | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | الوصف | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| إعادة التشغيل | Restarts the radio | -| Shutdown | Powers the radio down | -| Factory reset | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| إعادة التشغيل | Restarts the node | +| Shutdown | Powers the node down | +| Factory reset | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### حول @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ar-rSA/user/settings-radio-user.md b/docs/ar-rSA/user/settings-radio-user.md index 30ae17c8ce..f05f7db6a6 100644 --- a/docs/ar-rSA/user/settings-radio-user.md +++ b/docs/ar-rSA/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - الإعدادات - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | الوصف | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | الوصف | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | الوصف | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| الجهة | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | الوصف | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| الجهة | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | الوصف | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | الوصف | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | الوصف | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | الوصف | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | الوصف | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | الوصف | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | الوصف | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ar-rSA/user/signal-meter.md b/docs/ar-rSA/user/signal-meter.md index ac65bff2f7..597fc9768f 100644 --- a/docs/ar-rSA/user/signal-meter.md +++ b/docs/ar-rSA/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ar-rSA/user/tak.md b/docs/ar-rSA/user/tak.md index 4acacac5b2..bee4af1b08 100644 --- a/docs/ar-rSA/user/tak.md +++ b/docs/ar-rSA/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ar-rSA/user/telemetry-and-sensors.md b/docs/ar-rSA/user/telemetry-and-sensors.md index 736bfd9d50..df3eeca87e 100644 --- a/docs/ar-rSA/user/telemetry-and-sensors.md +++ b/docs/ar-rSA/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| المسافة | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| المسافة | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/ar-rSA/user/translate.md b/docs/ar-rSA/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/ar-rSA/user/translate.md +++ b/docs/ar-rSA/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ar-rSA/user/units-and-locale.md b/docs/ar-rSA/user/units-and-locale.md index 2136e1802d..9e549c45d8 100644 --- a/docs/ar-rSA/user/units-and-locale.md +++ b/docs/ar-rSA/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ar-rSA/user/widget.md b/docs/ar-rSA/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ar-rSA/user/widget.md +++ b/docs/ar-rSA/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/assets/screenshots/connections_empty_state.png b/docs/assets/screenshots/connections_empty_state.png deleted file mode 100644 index a6584d606e..0000000000 Binary files a/docs/assets/screenshots/connections_empty_state.png and /dev/null differ diff --git a/docs/be-rBY/user/app-functions.md b/docs/be-rBY/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/be-rBY/user/app-functions.md +++ b/docs/be-rBY/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/be-rBY/user/connections.md b/docs/be-rBY/user/connections.md index 585d2b94b5..b42eb327a4 100644 --- a/docs/be-rBY/user/connections.md +++ b/docs/be-rBY/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Сетка | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/be-rBY/user/debug-logs.md b/docs/be-rBY/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/be-rBY/user/debug-logs.md +++ b/docs/be-rBY/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/be-rBY/user/desktop.md b/docs/be-rBY/user/desktop.md index f8d415ffd5..853b76858e 100644 --- a/docs/be-rBY/user/desktop.md +++ b/docs/be-rBY/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/be-rBY/user/discovery.md b/docs/be-rBY/user/discovery.md index ba27fa0d1c..48f8edb77d 100644 --- a/docs/be-rBY/user/discovery.md +++ b/docs/be-rBY/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Апісанне | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Апісанне | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/be-rBY/user/firmware.md b/docs/be-rBY/user/firmware.md index a8b7b79e4d..40a71f36cb 100644 --- a/docs/be-rBY/user/firmware.md +++ b/docs/be-rBY/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/be-rBY/user/help-and-docs.md b/docs/be-rBY/user/help-and-docs.md index 453e3068d8..fde1a1f361 100644 --- a/docs/be-rBY/user/help-and-docs.md +++ b/docs/be-rBY/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/be-rBY/user/map-and-waypoints.md b/docs/be-rBY/user/map-and-waypoints.md index 63146a64df..a0a875dd43 100644 --- a/docs/be-rBY/user/map-and-waypoints.md +++ b/docs/be-rBY/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/be-rBY/user/messages-and-channels.md b/docs/be-rBY/user/messages-and-channels.md index 4980030fec..7a0a43c828 100644 --- a/docs/be-rBY/user/messages-and-channels.md +++ b/docs/be-rBY/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/be-rBY/user/mqtt.md b/docs/be-rBY/user/mqtt.md index 5c907f349f..39685e93c3 100644 --- a/docs/be-rBY/user/mqtt.md +++ b/docs/be-rBY/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/be-rBY/user/node-metrics.md b/docs/be-rBY/user/node-metrics.md index 9ede3163e1..bb24a8720b 100644 --- a/docs/be-rBY/user/node-metrics.md +++ b/docs/be-rBY/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/be-rBY/user/nodes.md b/docs/be-rBY/user/nodes.md index 218753f9f5..56aff13aea 100644 --- a/docs/be-rBY/user/nodes.md +++ b/docs/be-rBY/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | SENSOR | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK TRACKER | TAK position reporting only | -| LOST AND FOUND | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| LOST AND FOUND | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Фільтраваць | Апісанне | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фільтраваць | Апісанне | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Апісанне | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Апошні раз пачуты | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Адлегласць | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/be-rBY/user/notifications.md b/docs/be-rBY/user/notifications.md new file mode 100644 index 0000000000..56e8951d6b --- /dev/null +++ b/docs/be-rBY/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Паведамленні | Direct message notifications | A message sent directly to you | The conversation | +| Паведамленні | Broadcast message notifications | A message on one of your channels | The channel | +| Паведамленні | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Паведамленні | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Устройство | Service notifications | The connection to your node while the app runs in the background | The app | +| Устройство | Low battery notifications | Your node's battery running low | The node's details | +| Устройство | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Устройство | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/be-rBY/user/onboarding.md b/docs/be-rBY/user/onboarding.md index 679c13f0ae..95485590fc 100644 --- a/docs/be-rBY/user/onboarding.md +++ b/docs/be-rBY/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Начало работы -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/be-rBY/user/settings-module-admin.md b/docs/be-rBY/user/settings-module-admin.md index 7e53722a59..d6ffa33174 100644 --- a/docs/be-rBY/user/settings-module-admin.md +++ b/docs/be-rBY/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Module configuration Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Апісанне | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Імя карыстальніка | Authentication username | -| Пароль | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Апісанне | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Імя карыстальніка | Authentication username | +| Пароль | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Апісанне | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Апісанне | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Скончыўся час чакання | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Апісанне | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Апісанне | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Апісанне | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Апісанне | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Апісанне | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Апісанне | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Апісанне | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Апісанне | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Апісанне | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Апісанне | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | 3 | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Апісанне | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Апісанне | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Апісанне | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Апісанне | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Перазагрузіць | Restarts the radio | -| Shutdown | Powers the radio down | -| Factory reset | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Перазагрузіць | Restarts the node | +| Shutdown | Powers the node down | +| Factory reset | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### О нас @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/be-rBY/user/settings-radio-user.md b/docs/be-rBY/user/settings-radio-user.md index 90393181c7..d7fd1739f7 100644 --- a/docs/be-rBY/user/settings-radio-user.md +++ b/docs/be-rBY/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Апісанне | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Апісанне | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Уключана | | LED Heartbeat | Blink the status LED periodically | Уключана | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Апісанне | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Рэгіён | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Апісанне | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Рэгіён | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Апісанне | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| Тып OLED | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Апісанне | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| Тып OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Апісанне | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Апісанне | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Апісанне | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Пароль | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Апісанне | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Апісанне | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Прыватны ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Прыватны ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/be-rBY/user/signal-meter.md b/docs/be-rBY/user/signal-meter.md index 7321a5aa49..ca3338c081 100644 --- a/docs/be-rBY/user/signal-meter.md +++ b/docs/be-rBY/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/be-rBY/user/tak.md b/docs/be-rBY/user/tak.md index e70ca9bd38..68cc0a7754 100644 --- a/docs/be-rBY/user/tak.md +++ b/docs/be-rBY/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/be-rBY/user/telemetry-and-sensors.md b/docs/be-rBY/user/telemetry-and-sensors.md index d2187305d7..a3b975dd2e 100644 --- a/docs/be-rBY/user/telemetry-and-sensors.md +++ b/docs/be-rBY/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| SENSOR | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SENSOR | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | SENSOR | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Адлегласць | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Адлегласць | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/be-rBY/user/translate.md b/docs/be-rBY/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/be-rBY/user/translate.md +++ b/docs/be-rBY/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/be-rBY/user/units-and-locale.md b/docs/be-rBY/user/units-and-locale.md index 9ebe94a9de..85c492852a 100644 --- a/docs/be-rBY/user/units-and-locale.md +++ b/docs/be-rBY/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/be-rBY/user/widget.md b/docs/be-rBY/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/be-rBY/user/widget.md +++ b/docs/be-rBY/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/bg-rBG/user/app-functions.md b/docs/bg-rBG/user/app-functions.md index 0455fad3a6..d43d69d961 100644 --- a/docs/bg-rBG/user/app-functions.md +++ b/docs/bg-rBG/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: Ръководство за потребители nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/bg-rBG/user/connections.md b/docs/bg-rBG/user/connections.md index 30bfd7d6b5..ebd8a30230 100644 --- a/docs/bg-rBG/user/connections.md +++ b/docs/bg-rBG/user/connections.md @@ -1,8 +1,7 @@ --- title: Връзки -parent: Ръководство за потребители nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Свържете телефона или настолния си компютър с Meshtastic радио чрез Bluetooth, USB или TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Мрежа | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/bg-rBG/user/debug-logs.md b/docs/bg-rBG/user/debug-logs.md index 87a3bf20a8..57cca67b13 100644 --- a/docs/bg-rBG/user/debug-logs.md +++ b/docs/bg-rBG/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Ръководство за потребители nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/bg-rBG/user/desktop.md b/docs/bg-rBG/user/desktop.md index 5b8adbb570..ad736661e6 100644 --- a/docs/bg-rBG/user/desktop.md +++ b/docs/bg-rBG/user/desktop.md @@ -1,6 +1,5 @@ --- title: Настолно приложение -parent: Ръководство за потребители nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/bg-rBG/user/discovery.md b/docs/bg-rBG/user/discovery.md index 8388312f1a..6224c856d1 100644 --- a/docs/bg-rBG/user/discovery.md +++ b/docs/bg-rBG/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: Ръководство за потребители nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Преди да започнете, конфигурирайте тези контроли: -| Контрол | Описание | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Контрол | Описание | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/bg-rBG/user/firmware.md b/docs/bg-rBG/user/firmware.md index f367c2d1a6..eb7d25f43f 100644 --- a/docs/bg-rBG/user/firmware.md +++ b/docs/bg-rBG/user/firmware.md @@ -1,6 +1,5 @@ --- title: Актуализации на фърмуера -parent: Ръководство за потребители nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/bg-rBG/user/help-and-docs.md b/docs/bg-rBG/user/help-and-docs.md index c490d3b6c9..4eb819bf4c 100644 --- a/docs/bg-rBG/user/help-and-docs.md +++ b/docs/bg-rBG/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Ръководство за потребители nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/bg-rBG/user/map-and-waypoints.md b/docs/bg-rBG/user/map-and-waypoints.md index b348260d5a..8fc0d441f6 100644 --- a/docs/bg-rBG/user/map-and-waypoints.md +++ b/docs/bg-rBG/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: Ръководство за потребители nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Слоеве на картата -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/bg-rBG/user/messages-and-channels.md b/docs/bg-rBG/user/messages-and-channels.md index 51f82f81a8..79f8b8448e 100644 --- a/docs/bg-rBG/user/messages-and-channels.md +++ b/docs/bg-rBG/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Съобщения & Канали -parent: Ръководство за потребители nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/bg-rBG/user/mqtt.md b/docs/bg-rBG/user/mqtt.md index 710e446a7f..2258e7c930 100644 --- a/docs/bg-rBG/user/mqtt.md +++ b/docs/bg-rBG/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: Ръководство за потребители nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/bg-rBG/user/node-metrics.md b/docs/bg-rBG/user/node-metrics.md index 132c719dbf..dc999ada4e 100644 --- a/docs/bg-rBG/user/node-metrics.md +++ b/docs/bg-rBG/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: Ръководство за потребители nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -70,7 +69,7 @@ The BME680 **IAQ (Indoor Air Quality)** index is a single 0–500+ value derived Air Quality is a dedicated metrics view for nodes equipped with a particulate-matter and/or CO₂ sensor. It is **separate from the BME680 IAQ reading** listed under Environment Metrics — IAQ is a single gas-resistance-derived index, while the Air Quality view charts the underlying particulate and CO₂ measurements. -| Метрични | Unit | Описание | +| Метрични | Единица | Описание | | --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PM1.0 | µg/m³ | Particulate matter up to 1.0 micron | | PM2.5 | µg/m³ | Particulate matter up to 2.5 microns | @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/bg-rBG/user/nodes.md b/docs/bg-rBG/user/nodes.md index df241998f1..c17a32e7b9 100644 --- a/docs/bg-rBG/user/nodes.md +++ b/docs/bg-rBG/user/nodes.md @@ -1,8 +1,7 @@ --- title: Възли -parent: Ръководство за потребители nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Списък с възли +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Име на възела** — зададено от потребителя дълго име - **Кратко име** — идентификатор от 4 знака -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Последно чут** — време от последната комуникация - **Разстояние** — приблизително разстояние (ако се споделят местоположения) - **Батерия** — ниво на заряда на батерията на отдалечения възел (ако телеметрията е активирана) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Сензор | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Загубено и намерено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Загубено и намерено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Филтър | Описание | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Филтър | Описание | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Описание | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Последно чут | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Разстояние | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Значение | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/bg-rBG/user/notifications.md b/docs/bg-rBG/user/notifications.md new file mode 100644 index 0000000000..c5c186dfde --- /dev/null +++ b/docs/bg-rBG/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Известия +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Известия + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ---------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Съобщения | Известия за директни съобщения | A message sent directly to you | The conversation | +| Съобщения | Известия за излъчвани съобщения | A message on one of your channels | The channel | +| Съобщения | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Съобщения | Известия за предупреждения | A critical alert from a node | The conversation | +| Mesh | Известия за нови възли | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Известия за изтощена батерия (любими възли) | A favorite node's battery running low | The node's details | +| Устройство | Сервизни известия | The connection to your node while the app runs in the background | The app | +| Устройство | Известия за изтощена батерия | Your node's battery running low | The node's details | +| Устройство | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Устройство | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Свързани теми + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/bg-rBG/user/onboarding.md b/docs/bg-rBG/user/onboarding.md index 09892d02bf..8f8815a3a1 100644 --- a/docs/bg-rBG/user/onboarding.md +++ b/docs/bg-rBG/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Първи стъпки -parent: Ръководство за потребители nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/bg-rBG/user/settings-module-admin.md b/docs/bg-rBG/user/settings-module-admin.md index 104c04f18c..f01ce1afc2 100644 --- a/docs/bg-rBG/user/settings-module-admin.md +++ b/docs/bg-rBG/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: Ръководство за потребители nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Конфигурация на модулите +## Конфигуриране на модулите Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Настройка | Описание | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS е активиран | Use secure connection | | Root topic | Base MQTT topic path | -| Прокси към клиент е активиран | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT прокси на този телефон | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Прокси към клиент е активиран | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Настройка | Описание | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Съгласен съм. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Настройка | Описание | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Съгласен съм. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Настройка | Описание | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Външните известия са активирани | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Използване на PWM зумер | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Тон на звънене | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Настройка | Описание | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Външните известия са активирани | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Използване на PWM зумер | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Тон на звънене | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Настройка | Описание | -| -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Тест на обхвата е активиран | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Запазване на .CSV в хранилище (само за ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Настройка | Описание | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Тест на обхвата е активиран | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Запазване на .CSV в хранилище (само за ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Настройка | Описание | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Изпращане на телеметрия на устройството | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Интервал на актуализиране на показателите на устройството | How often to report battery, uptime and channel utilization | -| Модулът за измерване на околната среда е активиран | Report the attached environment sensors | -| Интервал на актуализиране на показателите за средата | How often to report them | -| Показателите на околната среда на екрана са активирани | Also show these readings on the device's own display | -| Показателите на околната среда използват Фаренхайт | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Модулът за показатели за качеството на въздуха е активиран | Report particulate and CO₂ sensor data | -| Интервал на актуализиране на показателите за качеството на въздуха | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Настройка | Описание | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Изпращане на телеметрия на устройството | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Интервал на актуализиране на показателите на устройството | How often to report battery, uptime and channel utilization | +| Модулът за измерване на околната среда е активиран | Report the attached environment sensors | +| Интервал на актуализиране на показателите за средата | How often to report them | +| Показателите на околната среда на екрана са активирани | Also show these readings on the device's own display | +| Показателите на околната среда използват Фаренхайт | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Модулът за показатели за качеството на въздуха е активиран | Report particulate and CO₂ sensor data | +| Интервал на актуализиране на показателите за качеството на въздуха | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Настройка | Описание | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Настройка | Описание | | ----------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Налични пинове | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Настройка | Описание | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Интервал на актуализиране (секунди) | How often to broadcast neighbor list | -| Предаване през LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Настройка | Описание | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Интервал на актуализиране | How often to broadcast neighbor list | +| Предаване през LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +215,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Текущ | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -226,39 +225,39 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | GPIO pin to monitor | GPIO pin connected to sensor | | Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | Използване на режим INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Send bell with alert message | Include bell character in alerts | | Приятелско име | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Настройка | Описание | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter е активиран | Activate people counting | -| Интервал на актуализиране (секунди) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Настройка | Описание | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter е активиран | Activate people counting | +| Интервал на актуализиране | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Действие | Какво прави | -| -------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Рестартиране | Restarts the radio | -| Изключване | Powers the radio down | -| Фабрично нулиране | Returns every setting to its factory default | -| Нулиране на базата данни с възли | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Действие | Какво прави | +| -------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Рестартиране | Restarts the node | +| Изключване | Powers the node down | +| Фабрично нулиране | Returns every setting to its factory default | +| Нулиране на базата данни с възли | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Архивиране & Възстановяване -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Разширени **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Почистване на базата данни с възлите -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Мерни единици** — метрични, имперски или според системата. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Относно @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/bg-rBG/user/settings-radio-user.md b/docs/bg-rBG/user/settings-radio-user.md index 6d43c41e4e..11e941d1a1 100644 --- a/docs/bg-rBG/user/settings-radio-user.md +++ b/docs/bg-rBG/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: Ръководство за потребители nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - настройки - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Настройка | Описание | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Дълго име | Your display name (up to 39 characters) | -| Кратко име | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Без съобщения | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Лицензиран радиолюбител (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Настройка | Описание | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Дълго име | Your display name (up to 39 characters) | +| Кратко име | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Без съобщения | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Режим на препредаване | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Активиран | | LED Heartbeat | Blink the status LED periodically | Активиран | | Часова зона | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Настройка | Описание | По подразбиране | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Предварително зададени | Speed/range tradeoff | LongFast | -| Брой отскоци | Maximum retransmit hops | 3 | -| Мощност на предаване | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Използване на предварително зададени настройки | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Широчина на честотната лента | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Честотен слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Предаването е активирано | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Игнориране на MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Настройка | Описание | По подразбиране | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Предварително зададени | Speed/range tradeoff | LongFast | +| Брой отскоци | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Използване на предварително зададени настройки | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Широчина на честотната лента | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Честотен слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Предаването е активирано | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Игнориране на MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Конфигуриране на дисплея -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Настройка | Описание | -| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Екранът е включен за | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Режим на дисплея | Screen layout/density used by the firmware | -| Показвани единици | Metric or Imperial on the radio's screen | -| Използване на 12ч формат | Show the radio's clock as 12-hour rather than 24-hour | -| Удебелен заглавен шрифт | Draw the screen's heading text in bold | -| Обръщане на екрана | Rotate the display 180° for an inverted mounting | -| Тип на OLED | Auto, SSD1306, SH1106, SH1107 | -| Събуждане при докосване или движение | Light the screen when the radio is tapped or moved | -| Ориентация на компаса | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Настройка | Описание | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Екранът е включен за | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Режим на дисплея | Screen layout/density used by the firmware | +| Показвани единици | Metric or Imperial on the node's screen | +| Използване на 12ч формат | Show the node's clock as 12-hour rather than 24-hour | +| Удебелен заглавен шрифт | Draw the screen's heading text in bold | +| Обръщане на екрана | Rotate the display 180° for an inverted mounting | +| Тип на OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Събуждане при докосване или движение | Light the screen when the node is tapped or moved | +| Ориентация на компаса | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Конфигуриране на позицията On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Настройка | Описание | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Режим на GPS (физически хардуер) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Интервал на излъчване | How often the position is shared with the mesh | | Интелигентна позиция | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Настройка | Описание | | --------------------------------------------------- | --------------------------------------------------------------- | -| Активиране на енергоспестяващ режим | Let the radio sleep aggressively between activity | +| Активиране на енергоспестяващ режим | Let the node sleep aggressively between activity | | Изключване при загуба на захранване | Power the device down after external power disappears | | Продължителност на супер дълбок сън | How long the deepest sleep state lasts | -| Минимално време за събуждане | The shortest time the radio stays awake once woken | +| Минимално време за събуждане | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Конфигуриране на мрежата -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Настройка | Описание | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Парола | Парола за мрежата | | Ethernet е активиран | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Конфигуриране на Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Настройка | Описание | | --------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Настройка | Описание | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Публичен ключ | Your node's public key (read-only) | | Администраторски ключ | Keys permitted to administer this node remotely — up to three | -| Частен ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Частен ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Регенериране на частния ключ | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Серийна конзола | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Управляем режим | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Управляем режим | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/bg-rBG/user/signal-meter.md b/docs/bg-rBG/user/signal-meter.md index 5493e53a98..e59e8d8a1d 100644 --- a/docs/bg-rBG/user/signal-meter.md +++ b/docs/bg-rBG/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: Ръководство за потребители nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/bg-rBG/user/tak.md b/docs/bg-rBG/user/tak.md index ad5b4c9cc7..24a860faa0 100644 --- a/docs/bg-rBG/user/tak.md +++ b/docs/bg-rBG/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: Ръководство за потребители nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/bg-rBG/user/telemetry-and-sensors.md b/docs/bg-rBG/user/telemetry-and-sensors.md index 7f11915889..70eba18d33 100644 --- a/docs/bg-rBG/user/telemetry-and-sensors.md +++ b/docs/bg-rBG/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Телеметрия & Сензори -parent: Ръководство за потребители nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ All Meshtastic nodes report basic device telemetry: ### Качество на въздуха -| Сензор | Метрични | Бележки | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Сензор | Метрични | Бележки | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Почва @@ -58,6 +58,8 @@ All Meshtastic nodes report basic device telemetry: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Светлина & UV | Сензор | Метрични | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Метрични | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Радиация | µR/h | Card and chart | -| Тегло | kg or lb | Card only — load cells, such as a beehive scale | -| Разстояние | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Метрични | Единица | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Радиация | µR/h | Card and chart | +| Тегло | kg or lb | Card only — load cells, such as a beehive scale | +| Разстояние | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Показатели на мощност @@ -116,12 +119,12 @@ hand-tune them for mesh size. Lengthen them deliberately only to save battery. Nodes with particulate matter or CO₂ sensors report air quality data: -| Метрични | Unit | Описание | -| --------------------- | ----- | ---------------------------- | -| PM1.0 | µg/m³ | Ultrafine particulate matter | -| PM2.5 | µg/m³ | Fine particulate matter | -| PM10 | µg/m³ | Coarse particulate matter | -| CO₂ | ppm | Carbon dioxide concentration | +| Метрични | Единица | Описание | +| --------------------- | ------- | ---------------------------- | +| PM1.0 | µg/m³ | Ultrafine particulate matter | +| PM2.5 | µg/m³ | Fine particulate matter | +| PM10 | µg/m³ | Coarse particulate matter | +| CO₂ | ppm | Carbon dioxide concentration | CO₂ sensors such as the SCD4x also report their own temperature and humidity, which appear alongside the readings above. From PM2.5 history the app additionally derives an **EPA NowCast AQI** value. diff --git a/docs/bg-rBG/user/translate.md b/docs/bg-rBG/user/translate.md index 6e50dc4b6b..2a972aee79 100644 --- a/docs/bg-rBG/user/translate.md +++ b/docs/bg-rBG/user/translate.md @@ -1,6 +1,5 @@ --- title: Преведете приложението -parent: Ръководство за потребителя nav_order: 17 last_updated: 2026-09-11 description: Как се превеждат приложението и документацията му чрез Crowdin, както и насоки за принос към преводите. diff --git a/docs/bg-rBG/user/units-and-locale.md b/docs/bg-rBG/user/units-and-locale.md index b644950144..3e0effb9c9 100644 --- a/docs/bg-rBG/user/units-and-locale.md +++ b/docs/bg-rBG/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. @@ -109,11 +108,11 @@ Rainfall measurements (1-hour and 24-hour totals) are transmitted as **mm** and Some units are international standards and are displayed the same way regardless of your locale: -| Measurement | Unit | Why | +| Measurement | Единица | Why | | -------------------------------- | ------------------------------ | ------------------------------------- | | Barometric pressure | hPa | International meteorological standard | | Heading / bearing | ° (degrees) | Universal navigation convention | -| Радиация | µR/h | Standard dosimetry unit | +| Радиация | µR/h | Стандартна дозиметрична единица | | GPS coordinates | decimal degrees | Universal geographic standard | | Humidity, battery, soil moisture | % | Universal | diff --git a/docs/bg-rBG/user/widget.md b/docs/bg-rBG/user/widget.md index 7c30dd1644..250d003e55 100644 --- a/docs/bg-rBG/user/widget.md +++ b/docs/bg-rBG/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Ръководство за потребители nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ca-rES/user/app-functions.md b/docs/ca-rES/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ca-rES/user/app-functions.md +++ b/docs/ca-rES/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ca-rES/user/connections.md b/docs/ca-rES/user/connections.md index 586e339991..418339c77c 100644 --- a/docs/ca-rES/user/connections.md +++ b/docs/ca-rES/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ca-rES/user/debug-logs.md b/docs/ca-rES/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ca-rES/user/debug-logs.md +++ b/docs/ca-rES/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ca-rES/user/desktop.md b/docs/ca-rES/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/ca-rES/user/desktop.md +++ b/docs/ca-rES/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ca-rES/user/discovery.md b/docs/ca-rES/user/discovery.md index 8d9f02a670..8af8bcf500 100644 --- a/docs/ca-rES/user/discovery.md +++ b/docs/ca-rES/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descripció | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descripció | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ca-rES/user/firmware.md b/docs/ca-rES/user/firmware.md index 605c8f68b8..606484bd2e 100644 --- a/docs/ca-rES/user/firmware.md +++ b/docs/ca-rES/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ca-rES/user/help-and-docs.md b/docs/ca-rES/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ca-rES/user/help-and-docs.md +++ b/docs/ca-rES/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ca-rES/user/map-and-waypoints.md b/docs/ca-rES/user/map-and-waypoints.md index a9502d1950..7c7bd25e86 100644 --- a/docs/ca-rES/user/map-and-waypoints.md +++ b/docs/ca-rES/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ca-rES/user/messages-and-channels.md b/docs/ca-rES/user/messages-and-channels.md index 8a300c40d9..18f8623ead 100644 --- a/docs/ca-rES/user/messages-and-channels.md +++ b/docs/ca-rES/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ca-rES/user/mqtt.md b/docs/ca-rES/user/mqtt.md index 80953fa2e4..3a3f256478 100644 --- a/docs/ca-rES/user/mqtt.md +++ b/docs/ca-rES/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ca-rES/user/node-metrics.md b/docs/ca-rES/user/node-metrics.md index 9dd4fbee89..ae5ac9244f 100644 --- a/docs/ca-rES/user/node-metrics.md +++ b/docs/ca-rES/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ca-rES/user/nodes.md b/docs/ca-rES/user/nodes.md index 865f413ef8..32c884bbc5 100644 --- a/docs/ca-rES/user/nodes.md +++ b/docs/ca-rES/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtre | Descripció | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Descripció | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descripció | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Última notícia | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distància | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ca-rES/user/notifications.md b/docs/ca-rES/user/notifications.md new file mode 100644 index 0000000000..2b624f6722 --- /dev/null +++ b/docs/ca-rES/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Notificacions de servei | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ca-rES/user/onboarding.md b/docs/ca-rES/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/ca-rES/user/onboarding.md +++ b/docs/ca-rES/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ca-rES/user/settings-module-admin.md b/docs/ca-rES/user/settings-module-admin.md index 4675093bf1..89948f1b7b 100644 --- a/docs/ca-rES/user/settings-module-admin.md +++ b/docs/ca-rES/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configuració de mòdul Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Descripció | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Descripció | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descripció | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descripció | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Temps esgotat | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descripció | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descripció | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descripció | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descripció | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descripció | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Descripció | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descripció | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descripció | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descripció | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descripció | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Descripció | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Descripció | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descripció | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descripció | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reiniciar | Restarts the radio | -| Apagar | Powers the radio down | -| Restauració dels paràmetres de fàbrica | Returns every setting to its factory default | -| Restablir NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reiniciar | Restarts the node | +| Apagar | Powers the node down | +| Restauració dels paràmetres de fàbrica | Returns every setting to its factory default | +| Restablir NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Sobre @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ca-rES/user/settings-radio-user.md b/docs/ca-rES/user/settings-radio-user.md index 0346a61695..de482185e7 100644 --- a/docs/ca-rES/user/settings-radio-user.md +++ b/docs/ca-rES/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descripció | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descripció | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descripció | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descripció | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descripció | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descripció | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descripció | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descripció | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descripció | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descripció | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descripció | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ca-rES/user/signal-meter.md b/docs/ca-rES/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/ca-rES/user/signal-meter.md +++ b/docs/ca-rES/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ca-rES/user/tak.md b/docs/ca-rES/user/tak.md index 25da3cd3b9..be948afc78 100644 --- a/docs/ca-rES/user/tak.md +++ b/docs/ca-rES/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ca-rES/user/telemetry-and-sensors.md b/docs/ca-rES/user/telemetry-and-sensors.md index 55af4e5627..b1e5e6893a 100644 --- a/docs/ca-rES/user/telemetry-and-sensors.md +++ b/docs/ca-rES/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distància | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distància | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/ca-rES/user/translate.md b/docs/ca-rES/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/ca-rES/user/translate.md +++ b/docs/ca-rES/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ca-rES/user/units-and-locale.md b/docs/ca-rES/user/units-and-locale.md index a09eee3f53..8ce5008e4a 100644 --- a/docs/ca-rES/user/units-and-locale.md +++ b/docs/ca-rES/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ca-rES/user/widget.md b/docs/ca-rES/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ca-rES/user/widget.md +++ b/docs/ca-rES/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/cs-rCZ/user/app-functions.md b/docs/cs-rCZ/user/app-functions.md index aca38fe93d..9b716b2933 100644 --- a/docs/cs-rCZ/user/app-functions.md +++ b/docs/cs-rCZ/user/app-functions.md @@ -1,6 +1,5 @@ --- title: Funkce aplikace -parent: Uživatelská příručka nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/cs-rCZ/user/connections.md b/docs/cs-rCZ/user/connections.md index 181b92eafb..f61e1cdf71 100644 --- a/docs/cs-rCZ/user/connections.md +++ b/docs/cs-rCZ/user/connections.md @@ -1,8 +1,7 @@ --- title: Připojení -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Síť | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/cs-rCZ/user/debug-logs.md b/docs/cs-rCZ/user/debug-logs.md index c7ecc45e64..57cca67b13 100644 --- a/docs/cs-rCZ/user/debug-logs.md +++ b/docs/cs-rCZ/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Uživatelská příručka nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/cs-rCZ/user/desktop.md b/docs/cs-rCZ/user/desktop.md index 5a4fd2b5d7..cebf54b09f 100644 --- a/docs/cs-rCZ/user/desktop.md +++ b/docs/cs-rCZ/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/cs-rCZ/user/discovery.md b/docs/cs-rCZ/user/discovery.md index 9f02302564..32075cf177 100644 --- a/docs/cs-rCZ/user/discovery.md +++ b/docs/cs-rCZ/user/discovery.md @@ -1,8 +1,7 @@ --- title: Vyhledávání zařízení v místní mesh síti -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Popis | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Popis | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informace o sousedech @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/cs-rCZ/user/firmware.md b/docs/cs-rCZ/user/firmware.md index a463aa2d4a..ee7d55d4d8 100644 --- a/docs/cs-rCZ/user/firmware.md +++ b/docs/cs-rCZ/user/firmware.md @@ -1,6 +1,5 @@ --- title: Aktualizace firmware -parent: Uživatelská příručka nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/cs-rCZ/user/help-and-docs.md b/docs/cs-rCZ/user/help-and-docs.md index b24e88bff1..8604e8d0a5 100644 --- a/docs/cs-rCZ/user/help-and-docs.md +++ b/docs/cs-rCZ/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Uživatelská příručka nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/cs-rCZ/user/map-and-waypoints.md b/docs/cs-rCZ/user/map-and-waypoints.md index 94f751c710..984f4096fc 100644 --- a/docs/cs-rCZ/user/map-and-waypoints.md +++ b/docs/cs-rCZ/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Mapové vrstvy -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/cs-rCZ/user/messages-and-channels.md b/docs/cs-rCZ/user/messages-and-channels.md index d87b138648..a80321afc3 100644 --- a/docs/cs-rCZ/user/messages-and-channels.md +++ b/docs/cs-rCZ/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/cs-rCZ/user/mqtt.md b/docs/cs-rCZ/user/mqtt.md index 65fcdefc87..f725661716 100644 --- a/docs/cs-rCZ/user/mqtt.md +++ b/docs/cs-rCZ/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/cs-rCZ/user/node-metrics.md b/docs/cs-rCZ/user/node-metrics.md index 9b6784b94f..3933fdf3a7 100644 --- a/docs/cs-rCZ/user/node-metrics.md +++ b/docs/cs-rCZ/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/cs-rCZ/user/nodes.md b/docs/cs-rCZ/user/nodes.md index b7e29ea0d4..f7ec65dce4 100644 --- a/docs/cs-rCZ/user/nodes.md +++ b/docs/cs-rCZ/user/nodes.md @@ -1,8 +1,7 @@ --- title: Uzly -parent: Uživatelská příručka nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Procházet, filtrovat a třídit uzly sítě – zobrazit podrobnosti, kvalitu signálu, role a rychlé akce. aliases: - node-list @@ -15,30 +14,34 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Indikátory stavu uzlu +### Node Status indicators -| Indikátor | Význam | -| ------------------------------ | ------------------------------------------------- | -| Zelený - čas posledního příjmu | Uzel byl slyšet během posledních 2 hodin | -| Běžný - čas posledního příjmu | Uzel nebyl slyšet déle než 2 hodiny | -| ⭐ Oblíbený | Uzel jste označili jako oblíbený. | +| Indikátor | Význam | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Zelený - čas posledního příjmu | Uzel byl slyšet během posledních 2 hodin | +| Běžný - čas posledního příjmu | Uzel nebyl slyšet déle než 2 hodiny | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Oblíbený | Uzel jste označili jako oblíbený. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. ### Role uzlu @@ -58,18 +61,18 @@ Uzlům lze nastavit různé role, které ovlivňují jejich chování v mesh sí | Senzor | Optimalizováno pro hlášení telemetrie | | TAK | Spolupracuje se systémy TAK (odesílá/přijímá CoT) | | TAK Tracker | Pouze odesílání polohy TAK | -| Lost and Found | Pravidelně odesílá svou polohu na výchozí kanál jako textovou zprávu, aby pomohl najít ztracené rádio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Výběr role +### Choosing a role Většina uživatelů by měla ponechat výchozí roli **Client**. Zvažte jinou roli, pokud: -- \*\*Router \*\*— Máte uzel na pevném, vyvýšeném místě se spolehlivým napájením (střecha, vrchol kopce). Routery zůstávají nepřetržitě aktivní, aby přeposílaly zprávy ostatních, a jsou klíčové pro rozšiřování pokrytí mesh sítě. Nepoužívejte roli Router u bateriově napájených ručních rádií. +- \*\*Router \*\*— Máte uzel na pevném, vyvýšeném místě se spolehlivým napájením (střecha, vrchol kopce). Routery zůstávají nepřetržitě aktivní, aby přeposílaly zprávy ostatních, a jsou klíčové pro rozšiřování pokrytí mesh sítě. Don't use Router on battery-powered handheld nodes. - **Router Late** — Infrastrukturní uzel, který vždy jednou přepošle paket, ale až poté, co dostaly prostor všechny ostatní směrovací režimy. Poskytuje doplňkové pokrytí místním skupinám uzlů, aniž by konkuroval hlavním routerům. - **Client Base** — Zachází s provozem z/na vaše oblíbené uzly s prioritou Router Late (zajišťuje těmto zprávám dodatečné přeposílání), zatímco vše ostatní zpracovává jako běžný Client. -- **Client Mute** — Chcete přijímat provoz mesh sítě, ale nechcete se podílet na jeho přeposílání. Vhodné pro rádia určená pouze k monitorování nebo ke snížení zahlcení v hustě osídlených oblastech. -- Neobsluhované rádio, jehož jediným účelem je vysílat svou GPS polohu (např. vozidlo, domácí mazlíček nebo sledovaný majetek). Sleeps between broadcasts to conserve battery. -- **Senzor** – Neobsluhované rádio které odesílá údaje z okolního prostředí (teplota, vlhkost, kvalita ovzduší). Podobná spotřeba energie jako Tracker. +- **Client Mute** — Chcete přijímat provoz mesh sítě, ale nechcete se podílet na jeho přeposílání. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Podobná spotřeba energie jako Tracker. - **TAK / TAK Tracker** – Potřebujete pouze pro spolupráci se systémy ATAK/WinTAK. Podrobnosti viz [TAK integrace](tak). > 💡 **Tip:** Mesh síť funguje nejlépe, když je většina uzlů v roli **Client** nebo **Router**. Příliš mnoho uzlů Client Mute snižuje odolnost mesh sítě; příliš mnoho routerů v hustě osídlené oblasti může způsobit zahlcení. Dobrým orientačním pravidlem je jeden Router na 5–10 uzlů Client ve vaší oblasti. @@ -78,19 +81,19 @@ Většina uživatelů by měla ponechat výchozí roli **Client**. Zvažte jinou Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Rychlé akce @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sdílet kontakt +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtr | Popis | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtr | Popis | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Popis | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Detail uzlu +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Naposledy slyšen | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Vzdálenost | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Význam | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Související témata diff --git a/docs/cs-rCZ/user/notifications.md b/docs/cs-rCZ/user/notifications.md new file mode 100644 index 0000000000..c7dfe027dc --- /dev/null +++ b/docs/cs-rCZ/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------- | +| Zprávy | Upozornění na přímou zprávu | A message sent directly to you | The conversation | +| Zprávy | Upozornění na hromadné zprávy | A message on one of your channels | The channel | +| Zprávy | Oznámení trasových bodů | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Zprávy | Upozornění na varování | A critical alert from a node | The conversation | +| Mesh | Oznámení o nových uzlech | A node heard for the first time | The node's details | +| Mesh | Oznámení o pozvánkách do mesh sítí | An invitation to join a nearby mesh | Vyhledávání zařízení v místní mesh síti | +| Mesh | Upozornění na nízký stav baterie (oblíbené uzly) | A favorite node's battery running low | The node's details | +| Zařízení | Servisní upozornění | The connection to your node while the app runs in the background | The app | +| Zařízení | Upozornění na nízký stav baterie | Your node's battery running low | The node's details | +| Zařízení | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Zařízení | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Související témata + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/cs-rCZ/user/onboarding.md b/docs/cs-rCZ/user/onboarding.md index 4280c918be..1cd493e6fd 100644 --- a/docs/cs-rCZ/user/onboarding.md +++ b/docs/cs-rCZ/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/cs-rCZ/user/settings-module-admin.md b/docs/cs-rCZ/user/settings-module-admin.md index c144ff8752..07387173bf 100644 --- a/docs/cs-rCZ/user/settings-module-admin.md +++ b/docs/cs-rCZ/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Nastavení modulů Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Nastavení | Popis | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT povoleno | Toggle MQTT bridge | -| Adresa | MQTT broker address | -| Uživatelské jméno | Authentication username | -| Heslo | Authentication password | -| Šifrování povoleno | Encrypt MQTT payloads | -| JSON výstup povolen | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS povoleno | Use secure connection | -| Kořenové téma | Base MQTT topic path | -| Proxy na klienta povoleno | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy na tomto telefonu | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Hlášení mapy | Publish position to the public map — see the Map reporting group that follows | +| Nastavení | Popis | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT povoleno | Toggle MQTT bridge | +| Adresa | MQTT broker address | +| Uživatelské jméno | Authentication username | +| Heslo | Authentication password | +| Šifrování povoleno | Encrypt MQTT payloads | +| JSON výstup povolen | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS povoleno | Use secure connection | +| Kořenové téma | Base MQTT topic path | +| Proxy na klienta povoleno | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Hlášení mapy | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: | Nastavení | Popis | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Souhlasím. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | +| Souhlasím. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | | Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Interval hlášení mapy | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +75,9 @@ Enables serial port communication for external device integrations (GPS modules, | Vypršel čas spojení | How long to wait before considering an incoming message complete | | Přepsat sériový port komunikace | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,11 +93,11 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | Výstupní pin vybračního motorku (GPIO) | Pin the vibration motor is wired to | | Použít PWM bzučák | Drive the buzzer with PWM, which allows tones rather than a single pitch | | Použít I2S jako bzučák | Send the alert through an I2S audio output instead | -| Doba trvání výstupu (v milisekundách) | How long a single alert lasts | -| Interval opakovaného zvonění | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | Vyzváněcí tón | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Nastavení | Popis | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| Test pokrytí povolen | Activate range testing | -| Interval odesílání zpráv | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Uložit .CSV do úložiště (pouze ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Nastavení | Popis | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| Test pokrytí povolen | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Uložit .CSV do úložiště (pouze ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Nastavení | Popis | -| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Odesílat telemetrii zařízení | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Interval aktualizace metrik zařízení | How often to report battery, uptime and channel utilization | -| Modul měření životního prostředí povolen | Report the attached environment sensors | -| Interval aktualizace měření životního prostředí | How often to report them | -| Zobrazení měření životního prostředí povoleno | Also show these readings on the device's own display | -| Měření životního prostředí používá Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Modul měření kvality ovzduší povolen | Report particulate and CO₂ sensor data | -| Interval aktualizace měření kvality ovzduší | How often to report them | -| Modul měření spotřeby povolen | Report the per-channel voltage and current readings | -| Interval aktualizace měření napájení | How often to report them | -| Měření spotřeby na obrazovce povoleno | Also show power readings on the device's display | +| Nastavení | Popis | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Odesílat telemetrii zařízení | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Interval aktualizace metrik zařízení | How often to report battery, uptime and channel utilization | +| Modul měření životního prostředí povolen | Report the attached environment sensors | +| Interval aktualizace měření životního prostředí | How often to report them | +| Zobrazení měření životního prostředí povoleno | Also show these readings on the device's own display | +| Měření životního prostředí používá Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Modul měření kvality ovzduší povolen | Report particulate and CO₂ sensor data | +| Interval aktualizace měření kvality ovzduší | How often to report them | +| Modul měření spotřeby povolen | Report the per-channel voltage and current readings | +| Interval aktualizace měření napájení | How often to report them | +| Měření spotřeby na obrazovce povoleno | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Nastavení | Popis | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Vstup Nahoru/Dolů/Výběr povolen | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Nastavení | Popis | | ------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Povolit přiřazení nedefinovaného pinu | Allow access to any GPIO pin (security risk) | | Dostupné piny | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Nastavení | Popis | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Informace o sousedech povoleny | Activate neighbor broadcasting | -| Interval aktualizace (v sekundách) | How often to broadcast neighbor list | -| Přenos přes LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Nastavení | Popis | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Informace o sousedech povoleny | Activate neighbor broadcasting | +| Interval aktualizace GPS | How often to broadcast neighbor list | +| Přenos přes LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Proud | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Nastavení | Popis | -| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detekční senzor povolen | Activate detection sensor | -| GPIO pin ke sledování | GPIO pin connected to sensor | -| Typ spouštění detekce | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Použít INPUT_PULLUP režim | Enable the pin's internal pull-up resistor | -| Minimální vysílání (sekundy) | Minimum time between alert broadcasts | -| Vysílání stavu (v sekundách) | Periodic state broadcast interval | -| Poslat zvonek s výstražnou zprávou | Include bell character in alerts | -| Přezdívka | Custom name for this sensor | +| Nastavení | Popis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detekční senzor povolen | Activate detection sensor | +| GPIO pin ke sledování | GPIO pin connected to sensor | +| Typ spouštění detekce | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Použít INPUT_PULLUP režim | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Poslat zvonek s výstražnou zprávou | Include bell character in alerts | +| Přezdívka | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Nastavení | Popis | -| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter povolen | Activate people counting | -| Interval aktualizace (v sekundách) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Nastavení | Popis | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter povolen | Activate people counting | +| Interval aktualizace GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------------------- | ------------------------------------------------------------------------------------------------------ | -| Nastavení času | Sends your phone's clock to the radio | -| Restartovat | Restarts the radio | -| Vypnout | Powers the radio down | -| Obnovení továrního nastavení | Returns every setting to its factory default | -| Reset NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------------------- | ---------------------------------------------------------------------------------------------- | +| Nastavení času | Sends your phone's clock to the node | +| Restartovat | Restarts the node | +| Vypnout | Powers the node down | +| Obnovení továrního nastavení | Returns every setting to its factory default | +| Reset NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Zálohování a obnovení -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Rozšířené **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Vyčistit databázi uzlů -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O aplikaci @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/cs-rCZ/user/settings-radio-user.md b/docs/cs-rCZ/user/settings-radio-user.md index 05ecb5641c..4a93dd1d6b 100644 --- a/docs/cs-rCZ/user/settings-radio-user.md +++ b/docs/cs-rCZ/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - nastavení - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Nastavení | Popis | -| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Dlouhé jméno | Your display name (up to 39 characters) | -| Krátké jméno | 4-character abbreviated name | -| Stavová zpráva | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Nepřijímá zprávy | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licencované amatérské rádio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Nastavení | Popis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dlouhé jméno | Your display name (up to 39 characters) | +| Krátké jméno | 4-character abbreviated name | +| Stavová zpráva | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Nepřijímá zprávy | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Režim opětovného vysílání | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Interval vysílání Node Info | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Dvojité klepnutí jako stisk tlačítka | Treat a double tap as a button press | Disabled | -| Okamžitý ping (trojitý stisk) | Send an ad-hoc position ping on a triple click | Disabled | +| Okamžitý ping (trojitý stisk) | Send an ad-hoc position ping on a triple click | Povoleno | | Blikání LED při provozu | Blink the status LED periodically | Povoleno | | Časové pásmo | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Nastavení | Popis | Výchozí | -| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Předvolby | Speed/range tradeoff | LongFast | -| Počet skoků | Maximum retransmit hops | 3 | -| Vysílací výkon | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Ruční nastavení frekvence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Použít předvolbu | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Šířka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frekvenční slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Vysílání povoleno | Turning this off makes the node receive-only | On | -| Přepsat pracovní cyklus | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Vypnuto | -| Ignorovat MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| OK do MQTT | Allow your packets to be forwarded to MQTT by gateways | Vypnuto | -| Zvýšené zesílení přijímače (RX) | Extra receive gain on SX126x radios; costs a little current | Vypnuto | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Vypnuto | +| Nastavení | Popis | Výchozí | +| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Předvolby | Speed/range tradeoff | LongFast | +| Počet skoků | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Ruční nastavení frekvence | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Použít předvolbu | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Šířka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frekvenční slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Vysílání povoleno | Turning this off makes the node receive-only | On | +| Přepsat pracovní cyklus | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Vypnuto | +| Ignorovat MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| OK do MQTT | Allow your packets to be forwarded to MQTT by gateways | Vypnuto | +| Zvýšené zesílení přijímače (RX) | Extra receive gain on SX126x radios; costs a little current | Vypnuto | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Vypnuto | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Nastavení zobrazení -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Nastavení | Popis | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Obrazovka zapnutá po dobu | How long the display stays lit before sleeping | -| Interval přepínání obrazovek | How often the radio cycles between screens on its own | -| Režim obrazovky | Screen layout/density used by the firmware | -| Zobrazení jednotek | Metric or Imperial on the radio's screen | -| Použít 12h formát hodin | Show the radio's clock as 12-hour rather than 24-hour | -| Tučný nadpis | Draw the screen's heading text in bold | -| Překlopit obrazovku | Rotate the display 180° for an inverted mounting | -| Typ OLED displeje | Auto, SSD1306, SH1106, SH1107 | -| Probuzení klepnutím nebo pohybem | Light the screen when the radio is tapped or moved | -| Orientace kompasu | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Vždy ukazovat na sever | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Nastavení | Popis | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Obrazovka zapnutá po dobu | How long the display stays lit before sleeping | +| Interval přepínání obrazovek | How often the node cycles between screens on its own | +| Režim obrazovky | Screen layout/density used by the firmware | +| Zobrazení jednotek | Metric or Imperial on the node's screen | +| Použít 12h formát hodin | Show the node's clock as 12-hour rather than 24-hour | +| Tučný nadpis | Draw the screen's heading text in bold | +| Překlopit obrazovku | Rotate the display 180° for an inverted mounting | +| Typ OLED displeje | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Probuzení klepnutím nebo pohybem | Light the screen when the node is tapped or moved | +| Orientace kompasu | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Vždy ukazovat na sever | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Nastavení pozice On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Nastavení | Popis | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Režim GPS (fyzický modul) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Interval aktualizace GPS | How often the radio asks its GPS for a fix | +| Interval aktualizace GPS | How often the node asks its GPS for a fix | | Interval vysílání | How often the position is shared with the mesh | | Chytrá poloha | Broadcast based on movement rather than purely on the clock | | Chytrý Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Nastavení | Popis | | ----------------------------------------------- | --------------------------------------------------------------- | -| Povolit úsporný režim | Let the radio sleep aggressively between activity | +| Povolit úsporný režim | Let the node sleep aggressively between activity | | Vypnutí při ztrátě napájení | Power the device down after external power disappears | | Doba super hlubokého spánku | How long the deepest sleep state lasts | -| Minimální doba probuzení | The shortest time the radio stays awake once woken | +| Minimální doba probuzení | The shortest time the node stays awake once woken | | Doba čekání na Bluetooth | How long to wait for a phone to connect before sleeping | | Přepsání násobiče ADC | Turn on a manual correction for battery-voltage readings | | Vlastní hodnota násobiče pro ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Nastavení sítě -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Nastavení | Popis | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Heslo | Network password | | Ethernet povolen | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Nastavení bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Nastavení | Popis | | ------------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Nastavení | Popis | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Veřejný klíč | Your node's public key (read-only) | | Administrátorský klíč | Keys permitted to administer this node remotely — up to three | -| Soukromý klíč | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Soukromý klíč | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Obnovit soukromý klíč | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Sériová komunikace | Serial console over the Stream API | -| Ladící protokol API povolen | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Řízený režim | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Ladící protokol API povolen | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Řízený režim | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Záloha klíčů | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Obnovit klíče | Write the backed-up keys back to the node (available once a backup exists) | | Smazat zálohu klíče | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/cs-rCZ/user/signal-meter.md b/docs/cs-rCZ/user/signal-meter.md index 563d9c2de5..5a2a911f26 100644 --- a/docs/cs-rCZ/user/signal-meter.md +++ b/docs/cs-rCZ/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/cs-rCZ/user/tak.md b/docs/cs-rCZ/user/tak.md index 4a0c0eacb1..e0c34fc5ad 100644 --- a/docs/cs-rCZ/user/tak.md +++ b/docs/cs-rCZ/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/cs-rCZ/user/telemetry-and-sensors.md b/docs/cs-rCZ/user/telemetry-and-sensors.md index 3db27664c8..1fde40def9 100644 --- a/docs/cs-rCZ/user/telemetry-and-sensors.md +++ b/docs/cs-rCZ/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Senzor | Metrický | Poznámka | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Senzor | Metrický | Poznámka | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Senzor | Metrický | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metrický | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiace | µR/h | Card and chart | -| Hmotnost | kg or lb | Card only — load cells, such as a beehive scale | -| Vzdálenost | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metrický | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiace | µR/h | Card and chart | +| Hmotnost | kg or lb | Card only — load cells, such as a beehive scale | +| Vzdálenost | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Metriky napájení diff --git a/docs/cs-rCZ/user/translate.md b/docs/cs-rCZ/user/translate.md index d033b222c8..d8eb0d156d 100644 --- a/docs/cs-rCZ/user/translate.md +++ b/docs/cs-rCZ/user/translate.md @@ -1,6 +1,5 @@ --- title: Překlad aplikace -parent: Uživatelská příručka nav_order: 17 last_updated: 2026-09-11 description: Jak se aplikace a její dokumentace překládají pomocí Crowdin a jak přispívat k překladům. diff --git a/docs/cs-rCZ/user/units-and-locale.md b/docs/cs-rCZ/user/units-and-locale.md index be1c524d25..5873e9f000 100644 --- a/docs/cs-rCZ/user/units-and-locale.md +++ b/docs/cs-rCZ/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/cs-rCZ/user/widget.md b/docs/cs-rCZ/user/widget.md index 53492bd92a..32d780d1a1 100644 --- a/docs/cs-rCZ/user/widget.md +++ b/docs/cs-rCZ/user/widget.md @@ -1,6 +1,5 @@ --- title: Widget na domovské obrazovce -parent: Uživatelská příručka nav_order: 20 last_updated: 2026-08-30 description: Přidejte widget Meshtastic na domovskou obrazovku a mějte přehled o místních statistikách připojeného rádia bez nutnosti otevírat aplikaci. diff --git a/docs/de-rDE/user/app-functions.md b/docs/de-rDE/user/app-functions.md index a3c88c713e..f434e9891e 100644 --- a/docs/de-rDE/user/app-functions.md +++ b/docs/de-rDE/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: Benutzerhandbuch nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/de-rDE/user/connections.md b/docs/de-rDE/user/connections.md index 4313ee671b..cc5ae7e82d 100644 --- a/docs/de-rDE/user/connections.md +++ b/docs/de-rDE/user/connections.md @@ -1,8 +1,7 @@ --- title: Verbindungen -parent: Benutzerhandbuch nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Verbinden Sie Ihr Telefon oder Ihren Desktop mit einem Meshtastic Funkgerät über Bluetooth, USB oder TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Netzwerk | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/de-rDE/user/debug-logs.md b/docs/de-rDE/user/debug-logs.md index 14571ef2a1..f5a8fe4b87 100644 --- a/docs/de-rDE/user/debug-logs.md +++ b/docs/de-rDE/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Fehlersuchprotokolle -parent: Benutzerhandbuch nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/de-rDE/user/desktop.md b/docs/de-rDE/user/desktop.md index 4c9ccd44d8..d3bc01559d 100644 --- a/docs/de-rDE/user/desktop.md +++ b/docs/de-rDE/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/de-rDE/user/discovery.md b/docs/de-rDE/user/discovery.md index e354e13e5c..87ef5439a3 100644 --- a/docs/de-rDE/user/discovery.md +++ b/docs/de-rDE/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery-Tools helfen Ihnen zu verstehen, **wie** Ihr Mesh-Netzwerk vernetzt is The app offers two complementary approaches: -- **Lokale Netzwerk Erkennung (Scanner)** – ein automatischer Modus, der das angeschlossene Funkmodul nacheinander auf verschiedene LoRa Voreinstellungen umschaltet, auf jeder dieser Einstellungen lauscht und bewertet, welche davon an Ihrem Standort die beste Leistung erbringt. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Beschreibung | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Beschreibung | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Phase | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Phase | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Nachbarinformation @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/de-rDE/user/firmware.md b/docs/de-rDE/user/firmware.md index 2928772a14..89dd837f40 100644 --- a/docs/de-rDE/user/firmware.md +++ b/docs/de-rDE/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmwareaktualisierungen -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/de-rDE/user/help-and-docs.md b/docs/de-rDE/user/help-and-docs.md index bddd7be107..8b5587b2ff 100644 --- a/docs/de-rDE/user/help-and-docs.md +++ b/docs/de-rDE/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Benutzerhandbuch nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/de-rDE/user/map-and-waypoints.md b/docs/de-rDE/user/map-and-waypoints.md index bf362012c6..9449faa58e 100644 --- a/docs/de-rDE/user/map-and-waypoints.md +++ b/docs/de-rDE/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Kartenebenen -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/de-rDE/user/messages-and-channels.md b/docs/de-rDE/user/messages-and-channels.md index c05d5adf5c..03f0800205 100644 --- a/docs/de-rDE/user/messages-and-channels.md +++ b/docs/de-rDE/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/de-rDE/user/mqtt.md b/docs/de-rDE/user/mqtt.md index 8d554c5088..e1f7757146 100644 --- a/docs/de-rDE/user/mqtt.md +++ b/docs/de-rDE/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Deaktiviert | | **TLS enabled** | Secure connection to broker | Deaktiviert | | **Map reporting** | Report position to public map | Deaktiviert | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Deaktiviert | +| **Proxy to client enabled** | Relay MQTT through the connected app | Deaktiviert | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/de-rDE/user/node-metrics.md b/docs/de-rDE/user/node-metrics.md index 12cff861ca..c2b7ce61b5 100644 --- a/docs/de-rDE/user/node-metrics.md +++ b/docs/de-rDE/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/de-rDE/user/nodes.md b/docs/de-rDE/user/nodes.md index fdbe8543bb..1cab84b416 100644 --- a/docs/de-rDE/user/nodes.md +++ b/docs/de-rDE/user/nodes.md @@ -1,8 +1,7 @@ --- title: Knoten -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Tracker | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Tracker | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Behandelt Datenverkehr von/zu Ihren bevorzugten Knoten mit der Priorität „Router Late“ (wodurch sichergestellt wird, dass diese Nachrichten eine zusätzliche Relaisabdeckung erhalten), während der übrige Verkehr wie bei einem normalen Client gehandhabt wird. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Symbol | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Symbol | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Beschreibung | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beschreibung | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sortieren | Beschreibung | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Zuletzt gehört | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distanz | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Bedeutung | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") Sobald die Hardware eines Knotens erkannt wird, zeigt die Detailansicht einen ausklappbaren Bereich mit der Bezeichnung **„Ich möchte eins“** an. Dieser enthält Links zu Bezugsquellen oder weiterführenden Informationen zu dem jeweiligen Gerät – etwa zur Produktseite des Herstellers, zu Produktvarianten sowie zu Angeboten auf regionalen Marktplätzen (wie AliExpress, Amazon und unterstützten Händlern), gefiltert nach Ihrem Land. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/de-rDE/user/notifications.md b/docs/de-rDE/user/notifications.md new file mode 100644 index 0000000000..ab969437e2 --- /dev/null +++ b/docs/de-rDE/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Benachrichtigungen +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Benachrichtigungen + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Kategorie | Posted for | Tapping it opens | +| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Nachrichten | Benachrichtigung direkte Nachrichten | A message sent directly to you | The conversation | +| Nachrichten | Benachrichtigung allgemeine Nachrichten | A message on one of your channels | The channel | +| Nachrichten | Wegpunkt Benachrichtigungen | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Nachrichten | Warnmeldungen | A critical alert from a node | The conversation | +| Netz | Benachrichtigung neue Knoten | A node heard for the first time | The node's details | +| Netz | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Netz | Akkustands Warnung (für Favoriten) | A favorite node's battery running low | The node's details | +| Gerät | Dienstbenachrichtigungen | The connection to your node while the app runs in the background | The app | +| Gerät | Benachrichtigung leerer Akku | Your node's battery running low | The node's details | +| Gerät | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Gerät | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Verwandte Themen + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/de-rDE/user/onboarding.md b/docs/de-rDE/user/onboarding.md index 3ff42493f6..7e33ade9a9 100644 --- a/docs/de-rDE/user/onboarding.md +++ b/docs/de-rDE/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Erste Schritte -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/de-rDE/user/settings-module-admin.md b/docs/de-rDE/user/settings-module-admin.md index 6cd5661d10..98dbe155bf 100644 --- a/docs/de-rDE/user/settings-module-admin.md +++ b/docs/de-rDE/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -30,39 +29,39 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Einstellung | Beschreibung | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT aktiviert | Toggle MQTT bridge | -| Adresse | MQTT broker address | -| Benutzername | Authentication username | -| Passwort | Authentication password | -| Verschlüsselung aktiviert | Encrypt MQTT payloads | -| JSON-Ausgabe aktiviert | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS aktiviert | Use secure connection | -| Hauptthema | Base MQTT topic path | -| Proxy zu Client aktiviert | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT Proxy auf diesem Handy | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Kartenberichte | Publish position to the public map — see the Map reporting group that follows | +| Einstellung | Beschreibung | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT aktiviert | Toggle MQTT bridge | +| Adresse | MQTT broker address | +| Benutzername | Authentication username | +| Passwort | Authentication password | +| Verschlüsselung aktiviert | Encrypt MQTT payloads | +| JSON-Ausgabe aktiviert | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS aktiviert | Use secure connection | +| Hauptthema | Base MQTT topic path | +| Proxy zu Client aktiviert | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Kartenberichte | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Einstellung | Beschreibung | -| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Ich stimme zu. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Kartenberichtsintervall (Sekunden) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Einstellung | Beschreibung | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ich stimme zu. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Zeitlimit erreicht | How long to wait before considering an incoming message complete | | Seriellen Anschluss der Konsole überschreiben | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Einstellung | Beschreibung | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Externe Benachrichtigungen aktiviert | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Ausgabe LED (GPIO) | Pin the LED is wired to | -| Ausgabe LED aktiv hoch | Whether the LED pin is active high or low | -| Ausgabe Summer (GPIO) | Pin the buzzer is wired to | -| Ausgabe Vibration (GPIO) | Pin the vibration motor is wired to | -| Benutze PWM Summer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| I2S als Buzzer verwenden | Send the alert through an I2S audio output instead | -| Ausgabedauer (GPIO) | How long a single alert lasts | -| Nervige Verzögerung (Sekunden) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Klingelton | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Einstellung | Beschreibung | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Externe Benachrichtigungen aktiviert | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Ausgabe LED (GPIO) | Pin the LED is wired to | +| Ausgabe LED aktiv hoch | Whether the LED pin is active high or low | +| Ausgabe Summer (GPIO) | Pin the buzzer is wired to | +| Ausgabe Vibration (GPIO) | Pin the vibration motor is wired to | +| Benutze PWM Summer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| I2S als Buzzer verwenden | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nervige Verzögerung | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Klingelton | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Einstellung | Beschreibung | -| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Reichweitentest aktiviert | Activate range testing | -| Sendernachrichtenintervall (Sekunden) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Speichere .CSV im Speicher (nur ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Einstellung | Beschreibung | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Reichweitentest aktiviert | Activate range testing | +| Sendeintervall | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Speichere .CSV im Speicher (nur ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Einstellung | Beschreibung | -| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Gerätetelemetrie senden | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Aktualisierungsintervall für Gerätedaten | How often to report battery, uptime and channel utilization | -| Modul Umweltdaten aktiviert | Report the attached environment sensors | -| Aktualisierungsintervall für Umweltdaten | How often to report them | -| Umweltdatenanzeige aktiviert | Also show these readings on the device's own display | -| Umweltdaten in Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Modul Luftqualität aktiviert | Report particulate and CO₂ sensor data | -| Aktualisierungsintervall der Luftqualität | How often to report them | -| Modul Energiedaten aktiviert | Report the per-channel voltage and current readings | -| Aktualisierungsintervall für Energiedaten | How often to report them | -| Energiedatenanzeige aktiviert | Also show power readings on the device's display | +| Einstellung | Beschreibung | +| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Gerätetelemetrie senden | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Aktualisierungsintervall für Gerätedaten | How often to report battery, uptime and channel utilization | +| Modul Umweltdaten aktiviert | Report the attached environment sensors | +| Aktualisierungsintervall für Umweltdaten | How often to report them | +| Umweltdatenanzeige aktiviert | Also show these readings on the device's own display | +| Umweltdaten in Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Modul Luftqualität aktiviert | Report particulate and CO₂ sensor data | +| Aktualisierungsintervall der Luftqualität | How often to report them | +| Modul Energiedaten aktiviert | Report the per-channel voltage and current readings | +| Aktualisierungsintervall für Energiedaten | How often to report them | +| Energiedatenanzeige aktiviert | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Einstellung | Beschreibung | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select Eingang aktiviert | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Einstellung | Beschreibung | | --------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Erlaube undefinierten Pin-Zugriff | Allow access to any GPIO pin (security risk) | | Verfügbare Pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Einstellung | Beschreibung | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| Nachbarinformationen aktiviert | Activate neighbor broadcasting | -| Aktualisierungsintervall (Sekunden) | How often to broadcast neighbor list | -| Übertragen über LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Einstellung | Beschreibung | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Nachbarinformationen aktiviert | Activate neighbor broadcasting | +| GPS Abfrageintervall | How often to broadcast neighbor list | +| Übertragen über LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Stromstärke | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Einstellung | Beschreibung | -| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Erkennungssensor aktiviert | Activate detection sensor | -| Zu überwachender GPIO-Pin | GPIO pin connected to sensor | -| Typ der Erkennungsauslösung | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Eingang PULLUP Einstellung | Enable the pin's internal pull-up resistor | -| Minimale Übertragungszeit (Sekunden) | Minimum time between alert broadcasts | -| Statusübertragung (Sekunden) | Periodic state broadcast interval | -| Glocke mit Warnmeldung senden | Include bell character in alerts | -| Anzeigename | Custom name for this sensor | +| Einstellung | Beschreibung | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Erkennungssensor aktiviert | Activate detection sensor | +| Zu überwachender GPIO-Pin | GPIO pin connected to sensor | +| Typ der Erkennungsauslösung | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Eingang PULLUP Einstellung | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| Übertragungsintervall | Periodic state broadcast interval | +| Glocke mit Warnmeldung senden | Include bell character in alerts | +| Anzeigename | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Einstellung | Beschreibung | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| Besucherzähler aktiviert | Activate people counting | -| Aktualisierungsintervall (Sekunden) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Einstellung | Beschreibung | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Besucherzähler aktiviert | Activate people counting | +| GPS Abfrageintervall | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. Siehe [TAK Integration](tak) für detaillierte Einrichtung und Nutzung. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Neustart | Restarts the radio | -| Herunterfahren | Powers the radio down | -| Auf Werkseinstellungen zurücksetzen | Returns every setting to its factory default | -| Node-Datenbank zurücksetzen | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Neustart | Restarts the node | +| Herunterfahren | Powers the node down | +| Auf Werkseinstellungen zurücksetzen | Returns every setting to its factory default | +| Node-Datenbank zurücksetzen | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Sichern & Wiederherstellen -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Fortgeschritten **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Knotendatenbank leeren -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Einstellungen -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Über @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/de-rDE/user/settings-radio-user.md b/docs/de-rDE/user/settings-radio-user.md index 8077bac73b..303b1f104f 100644 --- a/docs/de-rDE/user/settings-radio-user.md +++ b/docs/de-rDE/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - Einstellungen - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Einstellung | Beschreibung | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Langer Name | Your display name (up to 39 characters) | -| Kurzname | 4-character abbreviated name | -| Statusmeldung | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Nicht erreichbar | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Lizenzierter Amateurfunker | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Einstellung | Beschreibung | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Langer Name | Your display name (up to 39 characters) | +| Kurzname | 4-character abbreviated name | +| Statusmeldung | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Nicht erreichbar | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Weiterleitungsmodus | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Knoteninfo Übertragungsintervall | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Doppelklick als Taste | Treat a double tap as a button press | Deaktiviert | -| Dreifachklick für Ping | Send an ad-hoc position ping on a triple click | Deaktiviert | +| Dreifachklick für Ping | Send an ad-hoc position ping on a triple click | Aktiviert | | Puls LED | Blink the status LED periodically | Aktiviert | | Zeitzone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Einstellung | Beschreibung | Standardwert | -| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Voreinstellungen | Speed/range tradeoff | LongFast | -| Anzahl der Weiterleitungen | Maximum retransmit hops | 3 | -| Sendeleistung | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequenz überschreiben | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Voreinstellung verwenden | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spreizfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Fehlerkorrektur | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandbreite | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequenzschlitz | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Senden aktiviert | Turning this off makes the node receive-only | On | -| Duty-Cycle überschreiben | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Aus | -| MQTT ignorieren | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| OK für MQTT | Allow your packets to be forwarded to MQTT by gateways | Aus | -| Empfangsverstärkung | Extra receive gain on SX126x radios; costs a little current | Aus | -| PA Fan deaktiviert | Turn off the power-amplifier fan on hardware that has one | Aus | +| Einstellung | Beschreibung | Standardwert | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Voreinstellungen | Speed/range tradeoff | LongFast | +| Anzahl der Weiterleitungen | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequenz überschreiben | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Voreinstellung verwenden | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spreizfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Fehlerkorrektur | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandbreite | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequenzschlitz | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Senden aktiviert | Turning this off makes the node receive-only | On | +| Duty-Cycle überschreiben | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Aus | +| MQTT ignorieren | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| OK für MQTT | Allow your packets to be forwarded to MQTT by gateways | Aus | +| Empfangsverstärkung | Extra receive gain on SX126x radios; costs a little current | Aus | +| PA Fan deaktiviert | Turn off the power-amplifier fan on hardware that has one | Aus | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12,5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7,5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0,18 kbit/s | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Anzeigeeinstellungen -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Einstellung | Beschreibung | -| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Bildschirm eingeschaltet für | How long the display stays lit before sleeping | -| Karussellintervall | How often the radio cycles between screens on its own | -| Anzeigemodus | Screen layout/density used by the firmware | -| Anzeigeeinheiten | Metric or Imperial on the radio's screen | -| 12h Uhrformat verwenden | Show the radio's clock as 12-hour rather than 24-hour | -| Fette Überschrift | Draw the screen's heading text in bold | -| Bildschirm spiegeln | Rotate the display 180° for an inverted mounting | -| OLED Typ | Auto, SSD1306, SH1106, SH1107 | -| Aufwachen durch Tippen oder Bewegung | Light the screen when the radio is tapped or moved | -| Kompassausrichtung | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Immer nach Norden zeigen | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Einstellung | Beschreibung | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bildschirm eingeschaltet für | How long the display stays lit before sleeping | +| Karussellintervall | How often the node cycles between screens on its own | +| Anzeigemodus | Screen layout/density used by the firmware | +| Anzeigeeinheiten | Metric or Imperial on the node's screen | +| 12h Uhrformat verwenden | Show the node's clock as 12-hour rather than 24-hour | +| Fette Überschrift | Draw the screen's heading text in bold | +| Bildschirm spiegeln | Rotate the display 180° for an inverted mounting | +| OLED Typ | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Aufwachen durch Tippen oder Bewegung | Light the screen when the node is tapped or moved | +| Kompassausrichtung | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Immer nach Norden zeigen | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Standorteinstellungen On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Einstellung | Beschreibung | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS-Chip (Hardware) Modus | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Abfrageintervall | How often the radio asks its GPS for a fix | +| GPS Abfrageintervall | How often the node asks its GPS for a fix | | Übertragungsintervall | How often the position is shared with the mesh | | Intelligente Position | Broadcast based on movement rather than purely on the clock | | Intelligentes Intervall | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Einstellung | Beschreibung | | --------------------------------------------- | --------------------------------------------------------------- | -| Energiesparmodus aktivieren | Let the radio sleep aggressively between activity | +| Energiesparmodus aktivieren | Let the node sleep aggressively between activity | | Herunterfahren bei Stromausfall | Power the device down after external power disappears | | Dauer Supertiefschlaf | How long the deepest sleep state lasts | -| Minimale Aufwachzeit | The shortest time the radio stays awake once woken | +| Minimale Aufwachzeit | The shortest time the node stays awake once woken | | Zeit für Warten auf Bluetooth | How long to wait for a phone to connect before sleeping | | ADC Multiplikationsfaktor | Turn on a manual correction for battery-voltage readings | | ADC Multiplikator Überschreibungsverhältnis | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Netzwerkeinstellungen -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Einstellung | Beschreibung | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Passwort | Netzwerkpasswort | | Ethernet aktiviert | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Einstellungen -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Einstellung | Beschreibung | | ------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Einstellung | Beschreibung | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Öffentlicher Schlüssel | Your node's public key (read-only) | | Administrativer Schlüssel | Keys permitted to administer this node remotely — up to three | -| Privater Schlüssel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privater Schlüssel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Privaten Schlüssel neu erstellen | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serielle Konsole | Serial console over the Stream API | -| Debug-Protokoll-API aktiviert | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Verwalteter Modus | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug-Protokoll-API aktiviert | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Verwalteter Modus | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Schlüssel sichern | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/de-rDE/user/signal-meter.md b/docs/de-rDE/user/signal-meter.md index b08adf4f12..7649f14d23 100644 --- a/docs/de-rDE/user/signal-meter.md +++ b/docs/de-rDE/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: Wie das Meshtastic Signal Meter funktioniert -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: Wie das Signal Meter die Qualität von SNR relativ zum Modem preset bewertet - spread spectrum, presets, und was die Balken wirklich bedeuten. diff --git a/docs/de-rDE/user/tak.md b/docs/de-rDE/user/tak.md index 5bb1531318..8a76f7aae5 100644 --- a/docs/de-rDE/user/tak.md +++ b/docs/de-rDE/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/de-rDE/user/telemetry-and-sensors.md b/docs/de-rDE/user/telemetry-and-sensors.md index 11f1194236..d6de21307f 100644 --- a/docs/de-rDE/user/telemetry-and-sensors.md +++ b/docs/de-rDE/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metrisch | Knoten | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metrisch | Knoten | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metrisch | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metrisch | Einheit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Strahlung | µR/h | Card and chart | -| Gewicht | kg or lb | Card only — load cells, such as a beehive scale | -| Distanz | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metrisch | Einheit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Strahlung | µR/h | Card and chart | +| Gewicht | kg or lb | Card only — load cells, such as a beehive scale | +| Distanz | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Energiedaten diff --git a/docs/de-rDE/user/translate.md b/docs/de-rDE/user/translate.md index 7c25f5ef5b..535bc92ce8 100644 --- a/docs/de-rDE/user/translate.md +++ b/docs/de-rDE/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/de-rDE/user/units-and-locale.md b/docs/de-rDE/user/units-and-locale.md index cd96627d15..1a18ffcc03 100644 --- a/docs/de-rDE/user/units-and-locale.md +++ b/docs/de-rDE/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/de-rDE/user/widget.md b/docs/de-rDE/user/widget.md index 1c90fdbdcf..250d003e55 100644 --- a/docs/de-rDE/user/widget.md +++ b/docs/de-rDE/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Benutzerhandbuch nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/el-rGR/user/app-functions.md b/docs/el-rGR/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/el-rGR/user/app-functions.md +++ b/docs/el-rGR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/el-rGR/user/connections.md b/docs/el-rGR/user/connections.md index ebfeb214a1..5cf5c9c79f 100644 --- a/docs/el-rGR/user/connections.md +++ b/docs/el-rGR/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Δίκτυο | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/el-rGR/user/debug-logs.md b/docs/el-rGR/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/el-rGR/user/debug-logs.md +++ b/docs/el-rGR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/el-rGR/user/desktop.md b/docs/el-rGR/user/desktop.md index 1e8b760deb..84beac71c5 100644 --- a/docs/el-rGR/user/desktop.md +++ b/docs/el-rGR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/el-rGR/user/discovery.md b/docs/el-rGR/user/discovery.md index de09873c13..ea38ada95b 100644 --- a/docs/el-rGR/user/discovery.md +++ b/docs/el-rGR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Περιγραφή | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Περιγραφή | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/el-rGR/user/firmware.md b/docs/el-rGR/user/firmware.md index fa06e7dc41..81f58be0a8 100644 --- a/docs/el-rGR/user/firmware.md +++ b/docs/el-rGR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/el-rGR/user/help-and-docs.md b/docs/el-rGR/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/el-rGR/user/help-and-docs.md +++ b/docs/el-rGR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/el-rGR/user/map-and-waypoints.md b/docs/el-rGR/user/map-and-waypoints.md index 2fb1466f8c..5b2a15d74d 100644 --- a/docs/el-rGR/user/map-and-waypoints.md +++ b/docs/el-rGR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/el-rGR/user/messages-and-channels.md b/docs/el-rGR/user/messages-and-channels.md index c72efa049b..69475ed2b7 100644 --- a/docs/el-rGR/user/messages-and-channels.md +++ b/docs/el-rGR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/el-rGR/user/mqtt.md b/docs/el-rGR/user/mqtt.md index d06524c47d..14f1e1bbab 100644 --- a/docs/el-rGR/user/mqtt.md +++ b/docs/el-rGR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/el-rGR/user/node-metrics.md b/docs/el-rGR/user/node-metrics.md index 8d71925f67..b8b301fbae 100644 --- a/docs/el-rGR/user/node-metrics.md +++ b/docs/el-rGR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/el-rGR/user/nodes.md b/docs/el-rGR/user/nodes.md index 908c6d9290..e6c3f6fc44 100644 --- a/docs/el-rGR/user/nodes.md +++ b/docs/el-rGR/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Φίλτρο | Περιγραφή | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Φίλτρο | Περιγραφή | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Περιγραφή | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Last heard | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Απόσταση | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/el-rGR/user/notifications.md b/docs/el-rGR/user/notifications.md new file mode 100644 index 0000000000..97e0d05796 --- /dev/null +++ b/docs/el-rGR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Μηνύματα | Direct message notifications | A message sent directly to you | The conversation | +| Μηνύματα | Broadcast message notifications | A message on one of your channels | The channel | +| Μηνύματα | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Μηνύματα | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Συσκευή | Ειδοποιήσεις Υπηρεσίας | The connection to your node while the app runs in the background | The app | +| Συσκευή | Low battery notifications | Your node's battery running low | The node's details | +| Συσκευή | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Συσκευή | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/el-rGR/user/onboarding.md b/docs/el-rGR/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/el-rGR/user/onboarding.md +++ b/docs/el-rGR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/el-rGR/user/settings-module-admin.md b/docs/el-rGR/user/settings-module-admin.md index 0a8306884f..174aba64bc 100644 --- a/docs/el-rGR/user/settings-module-admin.md +++ b/docs/el-rGR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Ρυθμίσεις πρόσθετου Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Περιγραφή | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Διεύθυνση | MQTT broker address | -| Όνομα χρήστη | Authentication username | -| Κωδικός πρόσβασης | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Περιγραφή | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Διεύθυνση | MQTT broker address | +| Όνομα χρήστη | Authentication username | +| Κωδικός πρόσβασης | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Περιγραφή | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Περιγραφή | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Λήξη χρονικού ορίου | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Περιγραφή | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Περιγραφή | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Περιγραφή | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Περιγραφή | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Περιγραφή | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Περιγραφή | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Περιγραφή | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Περιγραφή | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Περιγραφή | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Περιγραφή | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Περιγραφή | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Περιγραφή | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Περιγραφή | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Περιγραφή | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| --------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Επανεκκίνηση | Restarts the radio | -| Τερματισμός λειτουργίας | Powers the radio down | -| Επαναφορά εργοστασιακών ρυθμίσεων | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| --------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Επανεκκίνηση | Restarts the node | +| Τερματισμός λειτουργίας | Powers the node down | +| Επαναφορά εργοστασιακών ρυθμίσεων | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Σχετικά @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/el-rGR/user/settings-radio-user.md b/docs/el-rGR/user/settings-radio-user.md index 751499a764..8402dc8bcf 100644 --- a/docs/el-rGR/user/settings-radio-user.md +++ b/docs/el-rGR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ρυθμίσεις - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Περιγραφή | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Περιγραφή | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Περιγραφή | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Περιφέρεια | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Περιγραφή | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Περιφέρεια | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Περιγραφή | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Περιγραφή | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Περιγραφή | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Περιγραφή | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Περιγραφή | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Κωδικός πρόσβασης | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Ρυθμίσεις Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Περιγραφή | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Περιγραφή | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Δημόσιο Κλειδί | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Ιδιωτικό Κλειδί | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Ιδιωτικό Κλειδί | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/el-rGR/user/signal-meter.md b/docs/el-rGR/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/el-rGR/user/signal-meter.md +++ b/docs/el-rGR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/el-rGR/user/tak.md b/docs/el-rGR/user/tak.md index 4834c0b87d..a71e84243f 100644 --- a/docs/el-rGR/user/tak.md +++ b/docs/el-rGR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/el-rGR/user/telemetry-and-sensors.md b/docs/el-rGR/user/telemetry-and-sensors.md index f3899262ac..44b7b42c77 100644 --- a/docs/el-rGR/user/telemetry-and-sensors.md +++ b/docs/el-rGR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Βάρος | kg or lb | Card only — load cells, such as a beehive scale | -| Απόσταση | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Βάρος | kg or lb | Card only — load cells, such as a beehive scale | +| Απόσταση | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/el-rGR/user/translate.md b/docs/el-rGR/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/el-rGR/user/translate.md +++ b/docs/el-rGR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/el-rGR/user/units-and-locale.md b/docs/el-rGR/user/units-and-locale.md index d9dcbe9a9c..c36a03e603 100644 --- a/docs/el-rGR/user/units-and-locale.md +++ b/docs/el-rGR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/el-rGR/user/widget.md b/docs/el-rGR/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/el-rGR/user/widget.md +++ b/docs/el-rGR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/en/developer.md b/docs/en/developer.md index 24ceeabce6..4287f27062 100644 --- a/docs/en/developer.md +++ b/docs/en/developer.md @@ -14,7 +14,7 @@ Technical documentation for contributing to the Meshtastic Android and Desktop a Things that trip up first-time contributors — check these before requesting review: - **Formatting passes** — run `./gradlew spotlessApply` to auto-format, then verify with `spotlessCheck` -- **Detekt passes** — run `./gradlew detekt` and fix all reported issues +- **Detekt passes** — run `./gradlew detekt detektTypeResolved` and fix all reported issues; the second task runs the rules that need the compile classpath - **All tests pass** — run `./gradlew test allTests` (both are needed: `test` covers Android-only modules, `allTests` covers KMP) - **Screenshot tests pass** — if you touched any Compose UI, run `./gradlew :screenshot-tests:validateDebugScreenshotTest` and update reference images if needed - **Protos are an external dependency** — protobuf models come from the `org.meshtastic:protobufs` Maven artifact (pinned in `gradle/libs.versions.toml`); change protos upstream and bump the version, never edit generated code locally diff --git a/docs/en/developer/adding-a-feature-module.md b/docs/en/developer/adding-a-feature-module.md index cf3bd8ee2f..f3b6c45790 100644 --- a/docs/en/developer/adding-a-feature-module.md +++ b/docs/en/developer/adding-a-feature-module.md @@ -2,7 +2,7 @@ title: Adding a Feature Module parent: Developer Guide nav_order: 3 -last_updated: 2026-08-29 +last_updated: 2026-09-29 description: Step-by-step guide for creating a new KMP feature module — module directory, build script, DI, routes, navigation entries, and the checklist. aliases: - new-module @@ -146,6 +146,9 @@ Every feature module should have: - [ ] Module directory created - [ ] `build.gradle.kts` with correct plugins and dependencies - [ ] Added to `settings.gradle.kts` +- [ ] Added to `ALL_MODULES_FULL` in `build-logic/convention/src/main/kotlin/RootConventionPlugin.kt`; `python3 scripts/check-module-list.py` fails when a non-exempt module is missing from it, when an entry is absent from the `settings.gradle.kts` includes, or when an exempt module is re-added, and a non-exempt module missing from it is absent from Dokka and Kover aggregation and `kmpSmokeCompile` +- [ ] Added to a test shard in `.github/workflows/reusable-check.yml` (`shard-feature` for a feature module): its `:feature:my-feature:allTests` task in `tasks` and its `koverXmlReport` in `kover`; `python3 scripts/check-test-shards.py` fails for a module with no test task in any shard, unless the module is listed in the script's `COVERED_ELSEWHERE` or `NO_TESTS_YET` sets +- [ ] If the module is user-facing, a page under `docs/en/user/` and a `MODULE_TO_DOCS` entry for it in `scripts/check-doc-coverage.js`; the check fails when a listed module's page is missing, and a module with no entry needs no page - [ ] DI module created with `@ComponentScan` - [ ] DI module registered in app and desktop roots - [ ] Routes added to `Routes.kt` diff --git a/docs/en/developer/architecture.md b/docs/en/developer/architecture.md index 137e010d2b..cb1c6219de 100644 --- a/docs/en/developer/architecture.md +++ b/docs/en/developer/architecture.md @@ -2,7 +2,7 @@ title: Architecture parent: Developer Guide nav_order: 1 -last_updated: 2026-09-11 +last_updated: 2026-09-29 description: How the Android and Desktop apps split into androidApp/desktopApp, feature modules, and core modules, and how radio control and navigation are layered across them. aliases: - layers @@ -58,6 +58,7 @@ Each `feature/` module owns a vertical slice of functionality: | `feature:connections` | Bluetooth/USB/TCP connection management | | `feature:map` | Map display, waypoints — shared state, policy and the waypoint editor | | `feature:map-maplibre` | MapLibre map surfaces — used by the `fdroid` flavor and Desktop; the `google` flavor uses Google Maps instead. Tile-source definitions and the custom-source editor are in `feature:map`, so both renderers share them | +| `feature:map-terrain` | Offline terrain: elevation decode, hillshade shading and contour lines, shared by both map flavors | | `feature:node` | Node list, node detail, metrics | | `feature:settings` | All configuration screens | | `feature:firmware` | Firmware update flow | @@ -68,7 +69,7 @@ Each `feature/` module owns a vertical slice of functionality: Feature modules: - Use the `meshtastic.kmp.feature` convention plugin -- Depend on `core` modules, never on other `feature` modules +- Depend on `core` modules, not on other `feature` modules; the one exception is `feature:map-maplibre`, which builds on `feature:map` and `feature:map-terrain` - Own their navigation entries and DI registrations - Contain platform-specific implementations in `androidMain`/`jvmMain`/`iosMain` @@ -107,14 +108,22 @@ Each module uses the standard KMP source set hierarchy: ```text src/ -├── commonMain/ ← Shared code (all platforms) -├── commonTest/ ← Shared tests -├── androidMain/ ← Android-specific -├── jvmMain/ ← Desktop JVM-specific -├── iosMain/ ← iOS-specific -└── jvmTest/ ← Desktop test host +├── commonMain/ ← Shared code (all platforms) +├── commonTest/ ← Shared tests +├── androidMain/ ← Android-specific +├── jvmMain/ ← Desktop JVM-specific +├── jvmAndroidMain/ ← Shared by Android and desktop JVM +├── iosMain/ ← iOS-specific +├── nativeMain/ ← Kotlin/Native code, built for the one iOS target (iosSimulatorArm64) +├── jvmTest/ ← Desktop test host +├── androidHostTest/ ← Android host (JVM) unit tests +└── androidDeviceTest/ ← Instrumented tests (core:database, core:model) ``` +`jvmAndroidMain` exists only in modules that apply `meshtastic.kmp.jvm.android`. `nativeMain`, +`androidHostTest` (modules that call `withHostTest`) and `androidDeviceTest` exist only in the +modules that need them. + **Golden Rules:** - No `android.*` imports in `commonMain` - Platform-specific code goes in appropriate source set diff --git a/docs/en/developer/codebase.md b/docs/en/developer/codebase.md index 81029dfc2b..2e1fd4b118 100644 --- a/docs/en/developer/codebase.md +++ b/docs/en/developer/codebase.md @@ -2,7 +2,7 @@ title: Codebase parent: Developer Guide nav_order: 2 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Repository layout, package namespacing, and the Gradle build system — convention plugins, build variants, and key tasks. aliases: - repository-layout @@ -29,6 +29,7 @@ Meshtastic-Android/ │ ├── connections/ │ ├── map/ │ ├── map-maplibre/ +│ ├── map-terrain/ │ ├── node/ │ ├── settings/ │ ├── firmware/ @@ -104,7 +105,7 @@ Located in `build-logic/convention/src/main/kotlin/`. The full set is registered | `meshtastic.kmp.feature` | Standard feature module setup | | `meshtastic.kmp.library` | Shared KMP library module | | `meshtastic.kmp.library.compose` | KMP library that also ships Compose UI | -| `meshtastic.kmp.jvm.android` | JVM + Android target configuration | +| `meshtastic.kmp.jvm.android` | Adds the `jvmAndroidMain` source set shared by the Android and desktop JVM targets | | `meshtastic.koin` | Koin Annotations + K2 compiler plugin | | `meshtastic.kotlinx.serialization` | Serialization plugin setup | | `meshtastic.android.room` | Room KMP setup and schema location | @@ -124,14 +125,14 @@ block rather than assuming a plugin does or does not exist. ### Key Gradle Tasks ```shell -# Compile check across all KMP targets +# Compile check of every KMP module for JVM and iosSimulatorArm64 (excludes :desktopApp), plus the device-test APKs ./gradlew kmpSmokeCompile -# Run all tests -./gradlew allTests +# Run all tests: allTests covers KMP modules, test covers Android/JVM-only modules; run both +./gradlew test allTests # Code quality -./gradlew spotlessCheck detekt +./gradlew spotlessCheck detekt detektTypeResolved # Android build ./gradlew assembleGoogleDebug assembleFdroidDebug @@ -143,7 +144,7 @@ block rather than assuming a plugin does or does not exist. ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS # API reference (Dokka HTML → build/dokka/html) -./gradlew dokkaGeneratePublicationHtml +./gradlew :dokkaGeneratePublicationHtml ``` ## Version Catalog Highlights diff --git a/docs/en/developer/contributing.md b/docs/en/developer/contributing.md index ce09fe50ae..bd8c672235 100644 --- a/docs/en/developer/contributing.md +++ b/docs/en/developer/contributing.md @@ -2,7 +2,7 @@ title: Contributing parent: Developer Guide nav_order: 8 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Branch naming, commit style, PR workflow, and the verification gates a change must pass before merge. aliases: - contributing @@ -41,7 +41,7 @@ Examples: 1. **Fork** the repository (external contributors) or create a branch (maintainers). 2. **Implement** your changes following the architecture guidelines. -3. **Test** locally: `./gradlew spotlessCheck detekt kmpSmokeCompile test allTests` +3. **Test** locally: `./gradlew spotlessCheck detekt detektTypeResolved kmpSmokeCompile test allTests` 4. **Commit** with clear, descriptive messages. 5. **Push** and open a Pull Request. @@ -61,7 +61,7 @@ Before submitting: - [ ] Code compiles on all targets: `./gradlew kmpSmokeCompile` - [ ] All tests pass: `./gradlew allTests` - [ ] Code style passes: `./gradlew spotlessCheck` -- [ ] Static analysis passes: `./gradlew detekt` +- [ ] Static analysis passes: `./gradlew detekt detektTypeResolved` - [ ] New code has appropriate test coverage - [ ] No `android.*` imports in `commonMain` - [ ] Koin modules registered if new DI is added @@ -92,7 +92,7 @@ Run formatting: Full pre-merge verification: ```shell -./gradlew spotlessCheck detekt kmpSmokeCompile test allTests +./gradlew spotlessCheck detekt detektTypeResolved kmpSmokeCompile test allTests ``` For docs-specific changes, also run: diff --git a/docs/en/developer/navigation-and-deep-links.md b/docs/en/developer/navigation-and-deep-links.md index e03e4976e2..c61c0d535b 100644 --- a/docs/en/developer/navigation-and-deep-links.md +++ b/docs/en/developer/navigation-and-deep-links.md @@ -87,8 +87,8 @@ manifest entry fails CI. | URI Path | Route | Notes | |----------|-------|-------| | `/connections` | `ConnectionsRoute.Connections(null)` | Connections screen | -| `/connections?address={prefixedAddress}` | `ConnectionsRoute.Connections(address)` | Auto-connects to a node without manual selection — the address uses the app's internal transport-prefixed format: `t192.168.1.1:4403` (TCP), `xAA:BB:CC:DD:EE:FF` (BLE), `s/dev/ttyUSB0` (serial). Intended for scripts/AI tooling driving the app. | -| `/connections?address=n` | `ConnectionsRoute.Connections("n")` | Disconnects the current node instead of connecting (`n` = the internal "no device selected" sentinel). | +| `/connections?address={prefixedAddress}` | `ConnectionsRoute.Connections(address)` | Connects to a node once the user confirms the "Connect to this device?" dialog — the address uses the app's internal transport-prefixed format: `t192.168.1.1:4403` (TCP), `xAA:BB:CC:DD:EE:FF` (BLE), `s/dev/ttyUSB0` (serial). Intended for scripts/AI tooling driving the app; a debug build started through its shell-only `org.meshtastic.app.AutomationLauncher` alias with the `skip_connect_confirm` intent extra, or with the `--skip-connect-confirm` argument on desktop, applies the address it was launched with without the dialog; links that arrive later still ask. | +| `/connections?address=n` | `ConnectionsRoute.Connections("n")` | Disconnects the current node instead of connecting (`n` = the internal "no device selected" sentinel), after the same confirmation. | | `/wifi-provision` | `WifiProvisionRoute.WifiProvision(null)` | Wi-Fi provisioning screen | | `/wifi-provision?address={mac}` | `WifiProvisionRoute.WifiProvision(mac)` | Provisioning targeting a specific node MAC | | `/settings` | `SettingsRoute.Settings(null)` | Settings root | @@ -110,6 +110,8 @@ manifest entry fails CI. | `/firmware` | `FirmwareRoute.FirmwareGraph` | Firmware screen | | `/firmware/update` | `FirmwareRoute.FirmwareUpdate` | Firmware update flow | +Demo Mode's addresses start with `m`. In a debug build, or once hidden features are unlocked, `/connections?address=m` connects to the demo mesh and `/connections?address=mshowcase` to the store-screenshot mesh, which the device picker never lists. `MockScenario` in `:core:network` holds both. + ### Backstack synthesis Deep links synthesize a full backstack, not just the target screen: diff --git a/docs/en/developer/persistence.md b/docs/en/developer/persistence.md index 603c98451c..64c32d88a5 100644 --- a/docs/en/developer/persistence.md +++ b/docs/en/developer/persistence.md @@ -2,7 +2,7 @@ title: Persistence parent: Developer Guide nav_order: 6 -last_updated: 2026-08-29 +last_updated: 2026-09-28 description: The app's three persistence layers — Room, DataStore, and core:prefs — and when a contributor should use each. aliases: - room @@ -55,6 +55,10 @@ The primary structured data store: | `DiscoveryPresetResultEntity` | Per-preset result within a discovery session | | `DiscoveredNodeEntity` | Nodes found during a discovery preset scan | | `DeviceLinkEntity` | Cached `msh.to` device links from the Meshtastic API | +| `EventFirmwareEditionEntity` | Event-firmware display records cached from the Meshtastic API (`/resource/eventFirmware`) | +| `BootloaderOtaQuirksCacheEntity` | Single-row cache of the nRF52 bootloader/OTA quirk catalog (`/resource/bootloaderOtaQuirks`), stored as one serialized envelope | +| `MaintenanceUf2CacheEntity` | Single-row cache of the maintenance-UF2 manifest (`/resource/maintenanceUf2`), stored as one serialized envelope | +| `MergeMarkerEntity` | Marks a completed `DatabaseMerger` merge so a re-run on the next connection skips it instead of duplicating rows | > ℹ️ **Note:** Waypoints and telemetry are stored within the `Packet` entity (the `port_num` field distinguishes packet types), alongside a `channel` index recording which channel each packet used. Channel *configuration* — names and LoRa settings — lives separately, in `ChannelSetEntity`. diff --git a/docs/en/developer/testing.md b/docs/en/developer/testing.md index cb302d502f..ac0de1a147 100644 Binary files a/docs/en/developer/testing.md and b/docs/en/developer/testing.md differ diff --git a/docs/en/user.md b/docs/en/user.md index f2336be269..97bf469302 100644 --- a/docs/en/user.md +++ b/docs/en/user.md @@ -17,6 +17,8 @@ Documentation for using the Meshtastic Android and Desktop app. Keep the last 5–8 entries and archive older ones by removing them. --> +**September 2026** — [Notifications](user/notifications) — A new page lists every notification category, what each opens when tapped, when a message stays quiet, and what a message notification lets you do from the shade or a watch. + **September 2026** — [Nodes](user/nodes) — Nodes your node has not heard since its LoRa settings changed now carry an orange marker, a **Hide unheard nodes** filter, and a banner whose **Remove** action deletes them in bulk. **September 2026** — [Settings — Radio & User](user/settings-radio-user) — Lockdown's debug-port lock is not reversible from the app, Managed Mode locks the whole configuration list, and the preset list is filtered to what your region permits. diff --git a/docs/en/user/connections.md b/docs/en/user/connections.md index 3adc991606..80b447e98f 100644 --- a/docs/en/user/connections.md +++ b/docs/en/user/connections.md @@ -2,7 +2,7 @@ title: Connections parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -44,7 +44,7 @@ The screen names anything on the app's side that is blocking a scan, with the fi | A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | | **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | | **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +63,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +|---|---| +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/en/user/debug-logs.md b/docs/en/user/debug-logs.md index c72700d1ed..0bbd23777e 100644 --- a/docs/en/user/debug-logs.md +++ b/docs/en/user/debug-logs.md @@ -2,7 +2,7 @@ title: Debug Logs parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +48,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/en/user/discovery.md b/docs/en/user/discovery.md index 6c3988a285..a1e4847348 100644 --- a/docs/en/user/discovery.md +++ b/docs/en/user/discovery.md @@ -168,7 +168,7 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. 4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. diff --git a/docs/en/user/firmware.md b/docs/en/user/firmware.md index 70573b9b21..14a9be9cab 100644 --- a/docs/en/user/firmware.md +++ b/docs/en/user/firmware.md @@ -76,6 +76,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/en/user/map-and-waypoints.md b/docs/en/user/map-and-waypoints.md index b777873679..f13f0cfe5c 100644 --- a/docs/en/user/map-and-waypoints.md +++ b/docs/en/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -100,7 +100,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -158,6 +158,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/en/user/mqtt.md b/docs/en/user/mqtt.md index adab74117e..7ac83df679 100644 --- a/docs/en/user/mqtt.md +++ b/docs/en/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -48,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -62,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -91,7 +89,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/en/user/node-metrics.md b/docs/en/user/node-metrics.md index afe552ffe8..9d10a36a2b 100644 --- a/docs/en/user/node-metrics.md +++ b/docs/en/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +152,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/en/user/nodes.md b/docs/en/user/nodes.md index 732807b593..149929ba47 100644 --- a/docs/en/user/nodes.md +++ b/docs/en/user/nodes.md @@ -193,6 +193,18 @@ Inline status indicators show key metrics at a glance: | Last heard | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distance | ![Distance](../../assets/screenshots/nodes_distance_info.png) | +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| --- | --- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + ### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. diff --git a/docs/en/user/notifications.md b/docs/en/user/notifications.md new file mode 100644 index 0000000000..6e4574e8b0 --- /dev/null +++ b/docs/en/user/notifications.md @@ -0,0 +1,73 @@ +--- +title: Notifications +parent: User Guide +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +|---|---|---|---| +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Service notifications | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/en/user/onboarding.md b/docs/en/user/onboarding.md index dcb7ce5f27..7254709e24 100644 --- a/docs/en/user/onboarding.md +++ b/docs/en/user/onboarding.md @@ -2,7 +2,7 @@ title: Getting Started parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -53,6 +53,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/en/user/settings-module-admin.md b/docs/en/user/settings-module-admin.md index ad9f57c397..7f7f776a5b 100644 --- a/docs/en/user/settings-module-admin.md +++ b/docs/en/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-09-19 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -30,7 +30,7 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the node — the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the node may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. ### MQTT module @@ -46,8 +46,8 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS enabled | Use secure connection | | Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed *Consent to Share Unencrypted Node Data @@ -58,7 +58,7 @@ until you agree: |---------|-------------| | I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | | Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads *±* the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. @@ -94,8 +94,8 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | Output vibra (GPIO) | Pin the vibration motor is wired to | | Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | | Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | ### Store & Forward module @@ -125,7 +125,7 @@ Automated range testing tool for evaluating link quality between nodes. When ena | Setting | Description | |---------|-------------| | Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | | Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | ### Telemetry module @@ -201,7 +201,7 @@ Broadcasts information about directly heard neighbors, enabling mesh topology ma | Setting | Description | |---------|-------------| | Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | +| Update Interval | How often to broadcast neighbor list | | Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. @@ -226,8 +226,8 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | GPIO pin to monitor | GPIO pin connected to sensor | | Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Send bell with alert message | Include bell character in alerts | | Friendly name | Custom name for this sensor | @@ -238,9 +238,9 @@ People counter using Wi-Fi and BLE probe requests. Counts nearby devices by pass | Setting | Description | |---------|-------------| | Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. @@ -292,8 +292,9 @@ Remotely configure nodes that share your admin key: ### Backup & Restore **Settings → Backup & Restore** writes the connected node's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one node's setup onto another. The section is shown for your +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your own node only, not over remote admin. ### Advanced @@ -331,6 +332,7 @@ appears only when your own node is selected. It is grouped rather than flat: - **Allow analytics and crash reporting** — opt in or out of diagnostics. - **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. - **Homoglyph encoding** — how look-alike characters in names are handled. **Appearance** diff --git a/docs/en/user/settings-radio-user.md b/docs/en/user/settings-radio-user.md index c503abeb92..4d4bcb2855 100644 --- a/docs/en/user/settings-radio-user.md +++ b/docs/en/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-19 +last_updated: 2026-09-28 description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -80,9 +80,10 @@ On **Settings → LoRa**. | Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | | Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as *Unsupported* and blocks saving until you pick a supported one | From preset | diff --git a/docs/en/user/tak.md b/docs/en/user/tak.md index 543c29764d..8eac88dba0 100644 --- a/docs/en/user/tak.md +++ b/docs/en/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -103,6 +103,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/en/user/telemetry-and-sensors.md b/docs/en/user/telemetry-and-sensors.md index 3cbb4a8200..b7a5281ef0 100644 --- a/docs/en/user/telemetry-and-sensors.md +++ b/docs/en/user/telemetry-and-sensors.md @@ -1,4 +1,4 @@ -| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres |--- +--- title: Telemetry & Sensors parent: User Guide nav_order: 9 @@ -76,7 +76,7 @@ Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, d | Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | | Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | | Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | | Radiation | µR/h | Card and chart | | Weight | kg or lb | Card only — load cells, such as a beehive scale | | Distance | mm or in | Card only — water level, from a distance sensor | diff --git a/docs/es-rES/user/app-functions.md b/docs/es-rES/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/es-rES/user/app-functions.md +++ b/docs/es-rES/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/es-rES/user/connections.md b/docs/es-rES/user/connections.md index 62cbf1acd7..9a45d8a8d6 100644 --- a/docs/es-rES/user/connections.md +++ b/docs/es-rES/user/connections.md @@ -1,8 +1,7 @@ --- title: Conexiones -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Conexión Red | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/es-rES/user/debug-logs.md b/docs/es-rES/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/es-rES/user/debug-logs.md +++ b/docs/es-rES/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/es-rES/user/desktop.md b/docs/es-rES/user/desktop.md index 55c447098a..6f0e12eaf3 100644 --- a/docs/es-rES/user/desktop.md +++ b/docs/es-rES/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/es-rES/user/discovery.md b/docs/es-rES/user/discovery.md index 651fcb2b8d..7248356c19 100644 --- a/docs/es-rES/user/discovery.md +++ b/docs/es-rES/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descripción | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descripción | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Información de Vecinos @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/es-rES/user/firmware.md b/docs/es-rES/user/firmware.md index cef398d79d..345f9f0e3d 100644 --- a/docs/es-rES/user/firmware.md +++ b/docs/es-rES/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/es-rES/user/help-and-docs.md b/docs/es-rES/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/es-rES/user/help-and-docs.md +++ b/docs/es-rES/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/es-rES/user/map-and-waypoints.md b/docs/es-rES/user/map-and-waypoints.md index 19bc7e2f3f..08987a18c1 100644 --- a/docs/es-rES/user/map-and-waypoints.md +++ b/docs/es-rES/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Capas del mapa -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/es-rES/user/messages-and-channels.md b/docs/es-rES/user/messages-and-channels.md index 1ea485b013..54ea05919a 100644 --- a/docs/es-rES/user/messages-and-channels.md +++ b/docs/es-rES/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/es-rES/user/mqtt.md b/docs/es-rES/user/mqtt.md index 32cfc30863..e8f3523087 100644 --- a/docs/es-rES/user/mqtt.md +++ b/docs/es-rES/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/es-rES/user/node-metrics.md b/docs/es-rES/user/node-metrics.md index 0f7f20692e..0d4cc76689 100644 --- a/docs/es-rES/user/node-metrics.md +++ b/docs/es-rES/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/es-rES/user/nodes.md b/docs/es-rES/user/nodes.md index 64ee135730..8cf9bb8a4d 100644 --- a/docs/es-rES/user/nodes.md +++ b/docs/es-rES/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodos -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | Rastreador TAK | TAK position reporting only | -| Perdido y encontrado | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Perdido y encontrado | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtro | Descripción | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descripción | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descripción | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Última escucha | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distancia | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/es-rES/user/notifications.md b/docs/es-rES/user/notifications.md new file mode 100644 index 0000000000..5a16a19655 --- /dev/null +++ b/docs/es-rES/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Mensajes | Notificaciones de mensajes directos | A message sent directly to you | The conversation | +| Mensajes | Notificaciones de mensaje de difusión | A message on one of your channels | The channel | +| Mensajes | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Mensajes | Notificaciones de alerta | A critical alert from a node | The conversation | +| Mesh | Notificaciones de nuevo nodo | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Notificaciones de batería baja (nodos favoritos) | A favorite node's battery running low | The node's details | +| Dispositivo | Notificaciones de servicio | The connection to your node while the app runs in the background | The app | +| Dispositivo | Notificaciones de batería baja | Your node's battery running low | The node's details | +| Dispositivo | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Dispositivo | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/es-rES/user/onboarding.md b/docs/es-rES/user/onboarding.md index 290fb605f1..a8eb4034a2 100644 --- a/docs/es-rES/user/onboarding.md +++ b/docs/es-rES/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Primeros Pasos -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/es-rES/user/settings-module-admin.md b/docs/es-rES/user/settings-module-admin.md index 78bf602053..b8501fe7a1 100644 --- a/docs/es-rES/user/settings-module-admin.md +++ b/docs/es-rES/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configuración de módulo Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Descripción | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Salida JSON activada | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS activado | Use secure connection | | Tema raíz | Base MQTT topic path | -| Compartir Internet a la Radio | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Compartir Internet a la Radio | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Reportar Posición | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descripción | -| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Estoy de acuerdo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Tiempo entre Reportes de Posición (en Segundos) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descripción | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Estoy de acuerdo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Tiempo agotado | How long to wait before considering an incoming message complete | | Sobreescribir el puerto serie de la consola | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descripción | -| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notificaciones externas activadas | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Salida LED (pin GPIO) | Pin the LED is wired to | -| LED de salida activo en alto | Whether the LED pin is active high or low | -| Salida buzzer (pin GPIO) | Pin the buzzer is wired to | -| Salida vibratoria (pin GPIO) | Pin the vibration motor is wired to | -| Utilizar buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Utilizar el Buzzer como uno I2S | Send the alert through an I2S audio output instead | -| Duración en las salidas (milisegundos) | How long a single alert lasts | -| | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Tono de notificación | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descripción | +| ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Notificaciones externas activadas | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Salida LED (pin GPIO) | Pin the LED is wired to | +| LED de salida activo en alto | Whether the LED pin is active high or low | +| Salida buzzer (pin GPIO) | Pin the buzzer is wired to | +| Salida vibratoria (pin GPIO) | Pin the vibration motor is wired to | +| Utilizar buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Utilizar el Buzzer como uno I2S | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Tono de notificación | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descripción | -| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Test de alcance activado | Activate range testing | -| Periodo entre los mensajes del transmisor (segundos) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Guardar el .CSV en el almacenamiento (Esp32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descripción | +| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Test de alcance activado | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Guardar el .CSV en el almacenamiento (Esp32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descripción | -| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Enviar telemetría del dispositivo | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Intervalo actualización de métricas del dispositivo | How often to report battery, uptime and channel utilization | -| Módulo para las medidas del entorno activado | Report the attached environment sensors | -| Intervalo actualización de métricas del entorno | How often to report them | -| Mostrar las medidas del entorno en la | Also show these readings on the device's own display | -| Grados Fahrenheit para la temperatura ambiente | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Módulo para la medición de la calidad del aire activado | Report particulate and CO₂ sensor data | -| Intervalo actualización de métricas calidad del aire | How often to report them | -| Módulo de medidas eléctricas activado | Report the per-channel voltage and current readings | -| Intervalo de actualización de métricas de energía | How often to report them | -| Medidas eléctricas en pantalla | Also show power readings on the device's display | +| Setting | Descripción | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Enviar telemetría del dispositivo | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Intervalo actualización de métricas del dispositivo | How often to report battery, uptime and channel utilization | +| Módulo para las medidas del entorno activado | Report the attached environment sensors | +| Intervalo actualización de métricas del entorno | How often to report them | +| Mostrar las medidas del entorno en la | Also show these readings on the device's own display | +| Grados Fahrenheit para la temperatura ambiente | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Módulo para la medición de la calidad del aire activado | Report particulate and CO₂ sensor data | +| Intervalo actualización de métricas calidad del aire | How often to report them | +| Módulo de medidas eléctricas activado | Report the per-channel voltage and current readings | +| Intervalo de actualización de métricas de energía | How often to report them | +| Medidas eléctricas en pantalla | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descripción | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Arriba/Abajo/Seleccionar Activado | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descripción | | -------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Permitir el acceso sin un pin definido | Allow access to any GPIO pin (security risk) | | Pines disponibles | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descripción | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Información de Vecinos | Activate neighbor broadcasting | -| Intervalo de refresco (segundos) | How often to broadcast neighbor list | -| Transmitir en LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descripción | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Información de Vecinos | Activate neighbor broadcasting | +| Intervalo de actualización | How often to broadcast neighbor list | +| Transmitir en LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Intensidad | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Descripción | -| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Sensor detector activado | Activate detection sensor | -| Pin GPIO para monitorizar | GPIO pin connected to sensor | -| Tipo de detección para activar | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Utilizar el modo de entrada PULL_UP | Enable the pin's internal pull-up resistor | -| Tiempo mínimo de transmisión (segundos) | Minimum time between alert broadcasts | -| Periodo entre transmisión de estado (segundos) | Periodic state broadcast interval | -| Mandar la campana con el mensaje de alerta | Include bell character in alerts | -| Mote | Custom name for this sensor | +| Setting | Descripción | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Sensor detector activado | Activate detection sensor | +| Pin GPIO para monitorizar | GPIO pin connected to sensor | +| Tipo de detección para activar | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Utilizar el modo de entrada PULL_UP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Mandar la campana con el mensaje de alerta | Include bell character in alerts | +| Mote | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descripción | -| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Activar el Contador de Paquetes | Activate people counting | -| Intervalo de refresco (segundos) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descripción | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Activar el Contador de Paquetes | Activate people counting | +| Intervalo de actualización | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reiniciar | Restarts the radio | -| Apagar | Powers the radio down | -| Restablecer los valores de fábrica | Returns every setting to its factory default | -| Reinicio de NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reiniciar | Restarts the node | +| Apagar | Powers the node down | +| Restablecer los valores de fábrica | Returns every setting to its factory default | +| Reinicio de NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Hacer copia de seguridad y restaurar -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avanzado **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Limpiar nodos de la base de datos -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Acerca de @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/es-rES/user/settings-radio-user.md b/docs/es-rES/user/settings-radio-user.md index 5c4884ad05..786aad7ac1 100644 --- a/docs/es-rES/user/settings-radio-user.md +++ b/docs/es-rES/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ajustes - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descripción | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nombre largo | Your display name (up to 39 characters) | -| Nombre Corto | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| No se puede enviar mensajes | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descripción | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nombre largo | Your display name (up to 39 characters) | +| Nombre Corto | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| No se puede enviar mensajes | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Modo de retransmisión | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Intervalo de transmisión de información del nodo | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Doble pulsación como botón | Treat a double tap as a button press | Disabled | -| Triple pulsación para enviar un ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple pulsación para enviar un ping | Send an ad-hoc position ping on a triple click | Habilitado | | Latido LED | Blink the status LED periodically | Habilitado | | Zona horaria | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descripción | Por defecto | -| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Predefinidos | Speed/range tradeoff | LongFast | -| Número de saltos | Maximum retransmit hops | 3 | -| Potencia de transmisión | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Sobreescribir frecuencia | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Usar predefinido | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Factor de dispersión | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Ratio de Codificación | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Ancho de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Banda de Frecuencia | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmisión habilitada | Turning this off makes the node receive-only | On | -| Sobreescribir el Tiempo de Trabajo | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignorar Paquetes MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Permitir MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| Aumentar ganancia de RX | Extra receive gain on SX126x radios; costs a little current | Off | -| Ventilador del Amplificador apagado | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descripción | Por defecto | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Predefinidos | Speed/range tradeoff | LongFast | +| Número de saltos | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Sobreescribir frecuencia | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Usar predefinido | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Factor de dispersión | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Ratio de Codificación | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Ancho de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Banda de Frecuencia | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmisión habilitada | Turning this off makes the node receive-only | On | +| Sobreescribir el Tiempo de Trabajo | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignorar Paquetes MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Permitir MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| Aumentar ganancia de RX | Extra receive gain on SX126x radios; costs a little current | Off | +| Ventilador del Amplificador apagado | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configuración de pantalla -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descripción | -| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Pantalla activa durante | How long the display stays lit before sleeping | -| Intervalo de carrusel | How often the radio cycles between screens on its own | -| Modo de la pantalla | Screen layout/density used by the firmware | -| Unidades en pantalla | Metric or Imperial on the radio's screen | -| Utilizar el formato de 12h para el reloj | Show the radio's clock as 12-hour rather than 24-hour | -| Encabezado en negrita | Draw the screen's heading text in bold | -| Girar la pantalla 180º | Rotate the display 180° for an inverted mounting | -| Tipo de OLED | Auto, SSD1306, SH1106, SH1107 | -| Despertar al tocar o al mover | Light the screen when the radio is tapped or moved | -| Orientación de la brújula | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Siempre apuntar al norte | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descripción | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pantalla activa durante | How long the display stays lit before sleeping | +| Intervalo de carrusel | How often the node cycles between screens on its own | +| Modo de la pantalla | Screen layout/density used by the firmware | +| Unidades en pantalla | Metric or Imperial on the node's screen | +| Utilizar el formato de 12h para el reloj | Show the node's clock as 12-hour rather than 24-hour | +| Encabezado en negrita | Draw the screen's heading text in bold | +| Girar la pantalla 180º | Rotate the display 180° for an inverted mounting | +| Tipo de OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Despertar al tocar o al mover | Light the screen when the node is tapped or moved | +| Orientación de la brújula | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Siempre apuntar al norte | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configuración de la posición On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descripción | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Modo GPS (dispositivo físico) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Intervalo de Difusión | How often the position is shared with the mesh | | Ubicación inteligente | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descripción | | -------------------------------------------------------------- | --------------------------------------------------------------- | -| Activar el modo ahorro de energía | Let the radio sleep aggressively between activity | +| Activar el modo ahorro de energía | Let the node sleep aggressively between activity | | Apagar al perder energía | Power the device down after external power disappears | | Duración del sueño súper profundo | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Esperar Bluetooth durante | How long to wait for a phone to connect before sleeping | | Sobreescribir multiplicador ADC | Turn on a manual correction for battery-voltage readings | | Sobreescribir relación del multiplicador ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configuración de la red -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descripción | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID (Nombre la Red) | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Contraseña | Contraseña de red | | Ethernet del Nodo Activado | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configuración Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descripción | | ---------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descripción | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Clave Pública | Your node's public key (read-only) | | Contraseña de administrador | Keys permitted to administer this node remotely — up to three | -| Clave privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Clave privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerar clave privada | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Consola serial | Serial console over the Stream API | -| API de registro de depuración habilitada | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Modo "ya terminado de configurar" | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API de registro de depuración habilitada | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Modo "ya terminado de configurar" | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/es-rES/user/signal-meter.md b/docs/es-rES/user/signal-meter.md index dda31f4ddb..de04b24957 100644 --- a/docs/es-rES/user/signal-meter.md +++ b/docs/es-rES/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/es-rES/user/tak.md b/docs/es-rES/user/tak.md index 196e18f426..de9e585c12 100644 --- a/docs/es-rES/user/tak.md +++ b/docs/es-rES/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/es-rES/user/telemetry-and-sensors.md b/docs/es-rES/user/telemetry-and-sensors.md index 2ba19e243e..8b46f7760f 100644 --- a/docs/es-rES/user/telemetry-and-sensors.md +++ b/docs/es-rES/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Métrico | Notas | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Métrico | Notas | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Métrico | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Métrico | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiación | µR/h | Card and chart | -| Peso | kg or lb | Card only — load cells, such as a beehive scale | -| Distancia | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Métrico | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiación | µR/h | Card and chart | +| Peso | kg or lb | Card only — load cells, such as a beehive scale | +| Distancia | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Métricas de Energía diff --git a/docs/es-rES/user/translate.md b/docs/es-rES/user/translate.md index 6b030d3673..1b74bfd619 100644 --- a/docs/es-rES/user/translate.md +++ b/docs/es-rES/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/es-rES/user/units-and-locale.md b/docs/es-rES/user/units-and-locale.md index 84abd8bf4a..f43d690cc5 100644 --- a/docs/es-rES/user/units-and-locale.md +++ b/docs/es-rES/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/es-rES/user/widget.md b/docs/es-rES/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/es-rES/user/widget.md +++ b/docs/es-rES/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/et-rEE/index.md b/docs/et-rEE/index.md index 7972d4c3b6..1dc89e1497 100644 --- a/docs/et-rEE/index.md +++ b/docs/et-rEE/index.md @@ -1,6 +1,6 @@ --- title: Kodu -layout: vaikimisi +layout: default nav_order: 0 --- diff --git a/docs/et-rEE/user/app-functions.md b/docs/et-rEE/user/app-functions.md index 99777f3005..353f5c4392 100644 --- a/docs/et-rEE/user/app-functions.md +++ b/docs/et-rEE/user/app-functions.md @@ -1,6 +1,5 @@ --- title: Rakenduse funktsioonid -parent: Kasutaja juhis nav_order: 19 last_updated: 2026-08-30 description: Ava kärgvõrgu funktsioonid Androidi süsteemile ja seadme tehisintellektil põhinevatele assistentidele (nt Gemini), et nad saaksid kärgvõrgu töövooge käivitada rakendust avamata. diff --git a/docs/et-rEE/user/connections.md b/docs/et-rEE/user/connections.md index 02988e7f1b..dbae9c4c35 100644 --- a/docs/et-rEE/user/connections.md +++ b/docs/et-rEE/user/connections.md @@ -1,8 +1,7 @@ --- title: Ühendus -parent: Kasutusjuhend nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Ühenda oma telefon või arvuti Meshtastic raadioga Bluetoothi, USB või TCP/IP kaudu. aliases: - sinihammas @@ -39,12 +38,12 @@ Sinihamba, võrgu ja USB-transpordi vahel vahetamiseks (üks on korraga aktiivne The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Ühenduse olek](../../assets/screenshots/connections_connecting.png) -Kui seadmeid ei leita, kuvab rakendus tühja oleku koos juhistega: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![Ühtegi seadet ei leitud](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sinihammas | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Võrk | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Sinihamba veaotsing diff --git a/docs/et-rEE/user/debug-logs.md b/docs/et-rEE/user/debug-logs.md index ba0d8532b8..d105cbc46c 100644 --- a/docs/et-rEE/user/debug-logs.md +++ b/docs/et-rEE/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Arendaja logid -parent: Kasutaja juhis nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Vaata ja ekspordi rakenduse arendajalogi rakenduse seest ning lisa GitHubi probleemile jäädvustus vigade diagnoosimiseks – adb-d pole vaja. aliases: - arendaja-logid @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Töölaud -Töölauarakendusel puudub süsteemi logcat, seega kuvatakse vahekaardil **Rakenduse logid** rakenduse enda jäädvustatud logide väljundit. Otsimine, filtreerimine ja eksportimine toimivad samamoodi. +Töölauarakendusel puudub süsteemi logcat, seega kuvatakse vahekaardil **Rakenduse logid** rakenduse enda jäädvustatud logide väljundit. Otsimine, filtreerimine ja eksportimine toimivad samamoodi. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Seotud teemad diff --git a/docs/et-rEE/user/desktop.md b/docs/et-rEE/user/desktop.md index 2ffdb26b66..e4eec53b8f 100644 --- a/docs/et-rEE/user/desktop.md +++ b/docs/et-rEE/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: Kasutusjuhend nav_order: 14 last_updated: 2026-09-11 description: Meshtastic arvuti rakendus pakub samu võrgusuhtluse funktsioone Linuxis, macOS-is ja Windowsis. diff --git a/docs/et-rEE/user/discovery.md b/docs/et-rEE/user/discovery.md index b451a90040..048af54bd3 100644 --- a/docs/et-rEE/user/discovery.md +++ b/docs/et-rEE/user/discovery.md @@ -1,8 +1,7 @@ --- title: Kohalik kärgvõrgu avastaja -parent: Kasutusjuhend nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Avasta oma kärgvõrku – kohalik kärgvõrgu avastaja skanner, traceroute'i teed, naabri-kaardid ja sõlmede avastamise tööriistad. aliases: - discovery @@ -20,53 +19,53 @@ Avastamistööriistad aitavad mõista, **kuidas** kärgvõrk on ühendatud – m The app offers two complementary approaches: -- **Kohalik kärgvõrgu avastaja (skanner)** – automaatne režiim, mis perioodiliselt skaneerib ühendatud raadiol läbi erinevate LoRa eelhäälestuste, kuulab igaüht neist ja järjestab, milline eelhäälestus sinu asukohas kõige paremini toimib. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manuaalne uurimine** – traceroute, naabri info ja sõlmede loend, mida saate igal ajal kasutada konkreetsete teede ja topoloogia uurimiseks. ## Kohalik kärgvõrgu avastaja (skanner) -Kohalik kärgvõrdu avastaja on spetsiaalne skaneerimisrežiim, mis aitab leida oma asukoha jaoks parima LoRa modemi eelseadistuse ja näha, millised sõlmed on igal eelseadistusel aktiivsed. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Kohalik kärgvõrdu avastaja on spetsiaalne skaneerimisrežiim, mis aitab leida oma asukoha jaoks parima LoRa modemi eelseadistuse ja näha, millised sõlmed on igal eelseadistusel aktiivsed. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Skannimise seadistamine +### Setting up a scan Before starting, configure these controls: -| Control | Kirjeldus | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **LoRa preset picker** | Vali skannimiseks üks või mitu eelseadistust. Otsing peatub kordamööda iga valitud eelseadistuse juures, et kuulata liiklust. | -| **Kuulamisaeg** | Time to listen on each preset. Vali 1, 5, 15, 30, 45, 60, 90, 120 või 180 minutit. Pikemad kuulamisajad koguvad rohkem pakette ja annavad selgema pildi, kuid võtavad kauem aega. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Kirjeldus | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Vali skannimiseks üks või mitu eelseadistust. Otsing peatub kordamööda iga valitud eelseadistuse juures, et kuulata liiklust. | +| **Kuulamisaeg** | Time to listen on each preset. Vali 1, 5, 15, 30, 45, 60, 90, 120 või 180 minutit. Pikemad kuulamisajad koguvad rohkem pakette ja annavad selgema pildi, kuid võtavad kauem aega. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - Skannimiseks pole **ühtegi eelseadet** valitud. - Valitud eelseadistus kasutab **2,4 GHz** sagedust, mida sinu riistvara ei toeta. -### Live Progress +### Live progress Skanni ajal näitab Discovery selle praegust etappi: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Praeguste sätete salvestamine ja skannimiseks valmistumine. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Kuulatakse praegust eelseadistust pakettide kogumiseks ja järgmise sammuni on oodata loendurit. | -| **Analyzing results** | Kogutud pakettide töötlemine ja eelseadete järjestamine. | -| **Restoring home preset** | Algsete LoRa seadete taastamine. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Praeguste sätete salvestamine ja skannimiseks valmistumine. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Kuulatakse praegust eelseadistust pakettide kogumiseks ja järgmise sammuni on oodata loendurit. | +| **Analyzing results** | Kogutud pakettide töötlemine ja eelseadete järjestamine. | +| **Restoring home preset** | Algsete LoRa seadete taastamine. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Kuulamis loendur näitab praeguse eelseadistuse järelejäänud aega](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results Kui skann on lõppenud, kuvab Discovery iga testitud eelseadistuse kohta tulemuste kaardi ja üldise kokkuvõtte. @@ -94,28 +93,28 @@ Tulemustest saadaolevad lisafunktsioonid: Kärgvõrgu majakas võimaldab sõlmedel kutsuda teisi oma võrguga liituma. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Kuula majakaid** — võta vastu teiste sõlmede edastatud kutseid. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Igal kaardil kuvatakse saatja sõnum koos pakutava kanali, piirkonna, eelseadistuse ja signaali kvaliteedi ning järgmiste toimingutega: -- **Liitu** — lülitu pakutavale kanalile ja seadista see eelhäälestamisega (häälestab raadio uuesti ja taaskäivitab selle). Kui pakkumine sobib praeguse sageduspesaga, lisab toiming **Lisa kanal** selle taaskäivituseta. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). Kui pakkumine sobib praeguse sageduspesaga, lisab toiming **Lisa kanal** selle taaskäivituseta. - **Avasta** – sisesta pakutud eelseadistusega avastusskannimisskeem, et saaksid enne liitumist seda võrku uurida (kuvatakse ainult siis, kui majakas pakub eelseadistust). - **Dismiss** — ignore the invitation. Majakate poolt reklaamitud kanalid kuvatakse skannimise seadistuses ka **Majakakanalitena** – valige üks, et see skannimise sihtmärgina lisada. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute näitab täpset teed, mida sõnum sõlmest mis tahes teise kärgvõrg 1. Mine valikuni **Sõlmed** ja puuduta sõlme, mida soovid jälgida. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results Traceroute'i tulemus näeb välja selline: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| Kõik hüpped näitavad head signaali-müra suhet (≥ −7 dB, roheline) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Kehv ühendus – see releesegment on habras | -| Mitu hüppet (4+) | Pikk tee – kaalu sõlme ümberpaigutamist selle lühendamiseks | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Kehv ühendus – see releesegment on habras | +| Mitu hüppet (4+) | Pikk tee – kaalu sõlme ümberpaigutamist selle lühendamiseks | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Vihje:** Käivita traceroute'i mitu korda mõne minuti tagant. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Kontrolli, et mõlemad sõlmed jagaksid vähemalt ühte kanalit sama krüpteerimisvõtmega. - **Traceroute aegus** — Tee võib olla liiga pikk (ületab hüppete limiidi) või on vahendussõlm ülekoormatud. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asümmeetrilised teed** – Jälgimismarsruut teelt A→B võib minna teist teed kui teelt B→A. This is normal — radio propagation is not always symmetric. +- **Asümmeetrilised teed** – Jälgimismarsruut teelt A→B võib minna teist teed kui teelt B→A. This is normal — radio propagation isn't always symmetric. ### Naabruse teave @@ -168,42 +167,42 @@ Naabriinfo moodul võimaldab igal sõlmel levitada nimekirja sõlmedest, mida se 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Luba moodul. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Teised sõlmed, millel on naabriinfo lubatud, teevad sama. -#### Naabri andmete vaatamine +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Iga naabri-kirje näitab otse kuuldud sõlme ja selle signaali kvaliteeti. - Kogu kärgvõrgu topoloogia mõistmiseks kombineerige mitme sõlme naaberandmeid. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Sõlmede loend avastusvahendina +### Node list as a discovery tool Sõlmede loend ise on võimas avastusvahend, kui kasutada selle filtreerimis- ja sortimisfunktsioone tõhusalt. -#### Otsin uusi sõlmi +#### Finding new nodes - Sorteeri **Viimati kuuldud** järgi, et näha kõige hiljutisemaid aktiivseid sõlmi ülaosas. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sorteeri **Hüpete arvu järgi**, et näha, millised sõlmed on otse kättesaadavad (0 hüpet) ja millised vahendatavate sõlmedega. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Kasuta **Välista MQTT** raadio teel (mitte internetisilla kaudu) ligipääsetavatele sõlmedele keskendumiseks. +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Kontrolli nende signaali kvaliteeti ja viimase kuulmise aegu, et veenduda oma infrastruktuuri sõlmede töökorras olekus. Filtreerimis- ja sortimisvalikute kohta leiate lisateavet jaotisest [Nodes](nodes). -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Alusta traceroute'ist** — see annab sulle kohest ja praktilist teavet konkreetse tee kohta. - **Luba naabriinfo funktsioon võtmesõlmedes** – eriti ruuterites ja repiiterites, et saada ülevaade magistraalvõrgust. diff --git a/docs/et-rEE/user/firmware.md b/docs/et-rEE/user/firmware.md index b7dd675def..b77c029a1a 100644 --- a/docs/et-rEE/user/firmware.md +++ b/docs/et-rEE/user/firmware.md @@ -1,6 +1,5 @@ --- title: Püsivara värskendus -parent: Kasutusjuhend nav_order: 13 last_updated: 2026-09-06 description: Raadio püsivara uuendamine sinihamba ​​või USB kaudu – OTA protsess, versioonikanalid, lennueelsed kontrollid ja taastamine. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as Rakendus loeb valitud kettalt faili `INFO_UF2.TXT`, et veenduda, kas see on tõepoolest seadme uuendusketas ja enne millegi kirjutamist plaat tuvastada. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/et-rEE/user/help-and-docs.md b/docs/et-rEE/user/help-and-docs.md index 9ac43fd472..b9c59da0c5 100644 --- a/docs/et-rEE/user/help-and-docs.md +++ b/docs/et-rEE/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Kasutaja juhis nav_order: 21 last_updated: 2026-09-11 description: Sirvi seda dokumentatsiooni rakenduses, otsi seda ja küsi Chirpylt – seadmesisesele TI assistendile – küsimusi Meshtasticu kohta. diff --git a/docs/et-rEE/user/map-and-waypoints.md b/docs/et-rEE/user/map-and-waypoints.md index 4a5494d993..7d5b797c80 100644 --- a/docs/et-rEE/user/map-and-waypoints.md +++ b/docs/et-rEE/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Kaart ja teekonnapunktid -parent: Kasutusjuhend nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Vaata sõlmede asukohti kaardil, loo ja jaga teekonnapunkte ning halda asukoha jagamist ja privaatsust. aliases: - kaart @@ -102,7 +101,7 @@ Kuna teekonnapunktid (ja nende geopiirded) edastatakse kogu kärgvõrgule, teavi ## Kaardikihid -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imporditud kihid on loetletud koos lülitiga iga kihi kuvamiseks/peitmiseks ja valikuga selle eemaldamiseks. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/et-rEE/user/messages-and-channels.md b/docs/et-rEE/user/messages-and-channels.md index dec9a0291d..3c16529f3e 100644 --- a/docs/et-rEE/user/messages-and-channels.md +++ b/docs/et-rEE/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Sõnumid ja kanalid -parent: Kasutusjuhend nav_order: 3 last_updated: 2026-09-14 description: Saada ja võta vastu sõnumeid, halda kanaleid, konfigureeri krüpteerimist ning kasuta kiirvestlust, reaktsioone ja sõnumitoiminguid. diff --git a/docs/et-rEE/user/mqtt.md b/docs/et-rEE/user/mqtt.md index 9a264ba6e7..ec88e253b0 100644 --- a/docs/et-rEE/user/mqtt.md +++ b/docs/et-rEE/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: Kasutusjuhend nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Silda oma võrk internetiga – MQTT maakleri seadistamine, krüpteerimiskihid ja kaardiaruandlus. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Keelatud | | **TLS enabled** | Secure connection to broker | Keelatud | | **Map reporting** | Teavita asukoht avalikul kaardil | Keelatud | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Keelatud | +| **Proxy to client enabled** | Relay MQTT through the connected app | Keelatud | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT puhverserver sellel telefonil +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Meshtastic vaikemaakler Kogukond haldab avaliku vahendajat aadressil `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privaatsus:** Avaliku vahendaja sõnumeid saavad lugeda kõik tellijad. Privaatse suhtluse jaoks kasuta alati kanali krüpteerimist. @@ -93,7 +90,7 @@ Konfigureeri oma sõlm nii, et see osutaks sinu privaatsele maaklerile sobivate When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/et-rEE/user/node-metrics.md b/docs/et-rEE/user/node-metrics.md index e7a0fed97a..cdb84dd98f 100644 --- a/docs/et-rEE/user/node-metrics.md +++ b/docs/et-rEE/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Sõlme mõõdikud -parent: Kasutusjuhend nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemeetria armatuurlauad iga võrgusõlme kohta – seadme tervis, keskkonnaandurid, õhu kvaliteet, signaali kvaliteet, võimsus, marsruut ja asukoha ajalugu. aliases: - meetriline @@ -152,7 +151,7 @@ Traceroute näitab sõnumi teed läbi kärgvõrgu: ### Traceroute'i tulemuste lugemine -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/et-rEE/user/nodes.md b/docs/et-rEE/user/nodes.md index 0d9ac8af43..0f47094e1e 100644 --- a/docs/et-rEE/user/nodes.md +++ b/docs/et-rEE/user/nodes.md @@ -1,8 +1,7 @@ --- title: Sõlmed -parent: Kasutusjuhend nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - sõlmede loend @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Sõlmede loend +## Node list -Sõlmede loend näitab kõiki sõlmi, mida raadio on kuulnud, sealhulgas: +The node list shows every node your node has heard, including: - **Sõlme nimi** — kasutaja pandud pikk nimi - **Lühinimi** — 4-tähemärgiline identifikaator -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Vahemaa** — hinnanguline vahemaa (kui asukohta jagatakse) - **Aku** — kaugsõlme aku tase (kui telemeetria on lubatud) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Sõlme oleku indikaatorid +### Node Status indicators -| Indicator | Tähendus | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Viimase 2 tunni jooksul kuuldud sõlm | -| Plain last-heard time | Viimase 2 tunni jooksul kuuldud sõlm | -| ⭐ Lemmik | Node you marked as a favorite. | +| Indicator | Tähendus | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Viimase 2 tunni jooksul kuuldud sõlm | +| Plain last-heard time | Viimase 2 tunni jooksul kuuldud sõlm | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Lemmik | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Sõlmedele saab määrata erinevaid rolle, mis mõjutavad nende kärgvõrgus käitumist: @@ -58,18 +61,18 @@ Sõlmedele saab määrata erinevaid rolle, mis mõjutavad nende kärgvõrgus kä | Andur | Optimized for telemetry reporting | | TAK | Ühildub TAK süsteemidega (saadab/võtab vastu CoT) | | Jälgitav TAK | Ainult TAK asukoha aruandlus | -| Kaotud ja leitud | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Kaotud ja leitud | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Ruuter** — Teil on sõlm fikseeritud, kõrgemal asuvas asukohas, millel on usaldusväärne toide (katusel, mäetipul). Ruuterid püsivad pidevalt ärkvel, et vahendada teistele sõnumeid ja on võrguühenduse laiendamiseks hädavajalikud. Don't use Router on battery-powered handheld radios. +- **Ruuter** — Teil on sõlm fikseeritud, kõrgemal asuvas asukohas, millel on usaldusväärne toide (katusel, mäetipul). Ruuterid püsivad pidevalt ärkvel, et vahendada teistele sõnumeid ja on võrguühenduse laiendamiseks hädavajalikud. Don't use Router on battery-powered handheld nodes. - **Ruuter hiline** – infrastruktuurisõlm, mis levitab pakette alati üks kord uuesti, aga alles pärast seda, kui kõik teised marsruutimisrežiimid on oma käigu teinud. Provides supplemental coverage for local clusters without competing with primary routers. - **Baas klient** – käsitleb lemmiksõlmedesse suunduvat ja sealt tulevaid liiklusi ruuteri hilinemise prioriteediga (tagades, et need sõnumid saavad täiendava edastuskatte), samal ajal kui kõike muud käsitletakse tavalise kliendina. -- **Kliendi vaigistatud** — Soovid vastu võtta võrguliiklust, aga mitte edastamisse panustada. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Aku säästmiseks magab saadete vahel. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Sarnane võimsusprofiil jälgimisseadmele. +- **Kliendi vaigistatud** — Soovid vastu võtta võrguliiklust, aga mitte edastamisse panustada. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Aku säästmiseks magab saadete vahel. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Sarnane võimsusprofiil jälgimisseadmele. - **TAK / TAK jälgimisseade** — Vajalik ainult ATAK/WinTAK süsteemidega koostööl. Üksikasjade saamiseks vaata [TAK integratsioon](tak). > 💡 **Vihje:** Kärgvõrk töötab kõige paremini, kui enamik sõlmi on **klient** või **ruuter**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Ikoon | Tähendus | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Ebakõla | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Ikoon | Tähendus | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Ebakõla | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Teksti otsing +### Text search Sõlmede filtreerimiseks nime või lühinime järgi tipi otsinguväljal. Filter uueneb reaalajas kirjutamise ajal. -### Filter Toggles +### Filter toggles -| Filtreeri | Kirjeldus | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Näita ainult viimase 2 tunni jooksul kuuldud sõlmi | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Välista MQTT** | Peida ainult MQTT internetisilla kaudu kuuldavad sõlmed | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtreeri | Kirjeldus | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Näita ainult viimase 2 tunni jooksul kuuldud sõlmi | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Välista MQTT** | Peida ainult MQTT internetisilla kaudu kuuldavad sõlmed | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sorteerimisvalikud +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sorteeri | Kirjeldus | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Sõlmede filtreerimiseks nime või lühinime järgi tipi otsinguväljal. Filter | **Distance** | Nearest nodes first (requires position sharing) | | **Hüppe kaugusel** | Vähim vahendatud hüppeid esimesena | | **Kanal** | Rühmitatud kanali loendi alusel | -| **via MQTT** | Rühmitatud MQTT ver raadiost kuuldud järgi | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Sõlme hüppe kohta +## Nodes per hop -Puuduta sõlmede loendi rakenduse ribal hüppehistogrammi ikooni, et avada tulpdiagramm, mis näitab, mitu sõlme asub igal hüppekaugusel (0 = otse, 1 = ühe relee kaugusel jne). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Puuduta sõlmede loendi rakenduse ribal hüppehistogrammi ikooni, et avada tulpdiagramm, mis näitab, mitu sõlme asub igal hüppekaugusel (0 = otse, 1 = ühe relee kaugusel jne). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Sõlmel klõpsamine avab detailvaate koos põhjaliku teabega. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Tekstisisesed olekuindikaatorid näitavad peamisi mõõdikuid lühidalt: | Viimati kuuldud | ![Viimati kuuldud](../../assets/screenshots/nodes_last_heard.png) | | Kaugus | ![Kaugus](../../assets/screenshots/nodes_distance_info.png) | -### Seadme lingid ("Soovin ühte") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Tähendus | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") Kui sõlme riistvara tuvastatakse, kuvatakse detailvaates kokkupandav jaotis **„Soovin ühte”**, mis lingib kohtadele, kust seadet osta või selle kohta lisateavet saada: müüja tooteleht, tootevariandid ja piirkondlike marketplace loendid (nt AliExpress, Amazon ja toetatud jaemüüjad), mis on filtreeritud sinu riigi järgi. Iga link avaneb ümbersuunamisteenuse `msh.to` kaudu. Seadmed, millel pole vastavaid linke, seda jaotist ei kuva. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Seotud teemad diff --git a/docs/et-rEE/user/notifications.md b/docs/et-rEE/user/notifications.md new file mode 100644 index 0000000000..30d9b50825 --- /dev/null +++ b/docs/et-rEE/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Märguanded +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Märguanded + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | --------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Sõnumid | Otsesõnumi märguanded | A message sent directly to you | The conversation | +| Sõnumid | Ringhäälingusõnumi teated | A message on one of your channels | The channel | +| Sõnumid | Teekonnapunktide teavitused | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Sõnumid | Märguteated | A critical alert from a node | The conversation | +| Kärgvõrk | Uue sõlme teade | A node heard for the first time | The node's details | +| Kärgvõrk | Kärgvõrgu kutse märguanne | An invitation to join a nearby mesh | Kohalik kärgvõrgu avastaja | +| Kärgvõrk | Madala akupinge teated (lemmik sõlmed) | A favorite node's battery running low | The node's details | +| Seade | Teenuse märguanded | The connection to your node while the app runs in the background | The app | +| Seade | Madala akupinge hoiatus | Your node's battery running low | The node's details | +| Seade | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Seade | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Seotud teemad + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/et-rEE/user/onboarding.md b/docs/et-rEE/user/onboarding.md index d8dd712ef4..d98545bf91 100644 --- a/docs/et-rEE/user/onboarding.md +++ b/docs/et-rEE/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: Kasutusjuhend nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Esimese käivitamise seadistus — õigused, sissejuhatav voog ja järgmised sammud pärast raadio ühendamist. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic kasutab sinu asukohta ka järgmiseks: - Calculating distances to other nodes - GPS koordinaatide jagamine teiste kärgvõrgu liikmetega (kui lubatud) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/et-rEE/user/settings-module-admin.md b/docs/et-rEE/user/settings-module-admin.md index 771ceeb5e0..cdeed4d1c6 100644 --- a/docs/et-rEE/user/settings-module-admin.md +++ b/docs/et-rEE/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Sätted - moodulid & admin -parent: Kasutusjuhend nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Muuda valikulisi funktsioonimooduleid (MQTT, telemeetria, salvestatud sõnumid, TAK ja palju muud) ja teosta seadme haldamist. aliases: - moodul @@ -14,7 +13,7 @@ aliases: Konfi valikulisi funktsioonimooduleid ja teosta seadme haldamist. Moodulid laiendavad Meshtasticut spetsiaalsete võimalustega – igaüht saab eraldi lubada või keelata. -> 💡 **Vihje:** Pead lubama ainult need moodulid, mida sa tegelikult kasutad. Kasutamata moodulite keelamine vähendab eetriaega, säästab akut ja lihtsustab seadistamist. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Vihje:** Pead lubama ainult need moodulid, mida sa tegelikult kasutad. Kasutamata moodulite keelamine vähendab eetriaega, säästab akut ja lihtsustab seadistamist. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Mooduli seaded kasutavad kaardipõhist paigutust koos lülitite, rippmenüüde, tekstiväljade ja liuguritega: @@ -26,43 +25,43 @@ Mooduli seaded kasutavad kaardipõhist paigutust koos lülitite, rippmenüüde, ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Mooduli konf +## Mooduli sätted Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT moodul +### MQTT module -Sildab võrgusõnumeid MQTT vahendajasse ja sealt internetiühenduse loomiseks. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Sildab võrgusõnumeid MQTT vahendajasse ja sealt internetiühenduse loomiseks. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Sätted | Kirjeldus | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT lubatud | Lükka MQTT sild sisse | -| Aadress | MQTT vahendaja aadress | -| Kasutajatunnus | Authentication username | -| Parool | Authentication password | -| Krüpteerimine lubatud | Krüpteeri MQTT kasutus | -| JSON väljund lubatud | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS lubatud | Use secure connection | -| Juurteema | Baas MQTT teema teekond | -| Kliendi proksi lubatud | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| Selle telefoni MQTT puhverserver | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Kaardi raport | Publish position to the public map — see the Map reporting group that follows | +| Sätted | Kirjeldus | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT lubatud | Lükka MQTT sild sisse | +| Aadress | MQTT vahendaja aadress | +| Kasutajatunnus | Authentication username | +| Parool | Authentication password | +| Krüpteerimine lubatud | Krüpteeri MQTT kasutus | +| JSON väljund lubatud | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS lubatud | Use secure connection | +| Juurteema | Baas MQTT teema teekond | +| Kliendi proksi lubatud | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Kaardi raport | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Sätted | Kirjeldus | -| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nõustun. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Kaardi raporti sagedus (sekund) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Sätted | Kirjeldus | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nõustun. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | Vaata [MQTT](mqtt) üksikasjalikumat kasutusjuhendit, mis sisaldab teavet krüpteerimise, privaatsuse ja vahendaja seadistamise kohta,. -### Jadapordi moodul +### Serial module Võimaldab jadapordi sidet väliste seadmete integreerimiseks (GPS-moodulid, andurid või kohandatud riistvara). Kui lubatud, saab sõlme jadaühendus saata ja vastu võtta protobuf- või tekstiandmeid, võimaldades välistel mikrokontrolleritel või arvutitel võrguga suhelda. @@ -76,29 +75,29 @@ Võimaldab jadapordi sidet väliste seadmete integreerimiseks (GPS-moodulid, and | Aegunud | How long to wait before considering an incoming message complete | | Konsooli jadapordi alistamine | Take over the port the debug console normally uses | -### Välise teavitusmoodul +### External Notification module -Juhib raadio riistvara summeri-, LED- või vibratsioonihoiatusi. Kasulik seadmetele, mis peavad sõnumi saabumisest füüsiliselt märku andma – eriti kasulik järelevalveta või välistingimustes paigaldamise korral. +Controls buzzer, LED, or vibration alerts on your node hardware. Kasulik seadmetele, mis peavad sõnumi saabumisest füüsiliselt märku andma – eriti kasulik järelevalveta või välistingimustes paigaldamise korral. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Sätted | Kirjeldus | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Luba Välised teated | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Väljund LED (GPIO) | Pin the LED is wired to | -| Väljund LED aktiivne | Whether the LED pin is active high or low | -| Väljund summer (GPIO) | Pin the buzzer is wired to | -| Väljund värin (GPIO) | Pin the vibration motor is wired to | -| Kasuta PWM summerit | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Kasuta I2S summerina | Send the alert through an I2S audio output instead | -| Väljundi kestvus (millisekundit) | How long a single alert lasts | -| Häire ajalõpp (sekundit) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Helin | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Sätted | Kirjeldus | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Luba Välised teated | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Väljund LED (GPIO) | Pin the LED is wired to | +| Väljund LED aktiivne | Whether the LED pin is active high or low | +| Väljund summer (GPIO) | Pin the buzzer is wired to | +| Väljund värin (GPIO) | Pin the vibration motor is wired to | +| Kasuta PWM summerit | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Kasuta I2S summerina | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Helin | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Salvesta & edasta moodul +### Store & Forward module Puhverdab ajutiselt võrguühenduseta olnud sõlmede sõnumeid ja esitab need uuesti, kui need sõlmed taasühenduvad. Hädavajalik kärgvõrgu jaoks, kus sõlmed regulaarselt levialasse lähevad ja levialast välja lähevad — tagab, et lühikeste katkestuste ajal sõnumid kaotsi ei lähe. @@ -113,7 +112,7 @@ Puhverdab ajutiselt võrguühenduseta olnud sõlmede sõnumeid ja esitab need uu > 💡 **Vihje:** Salvesta ja edasta töötab kõige paremini rohke mäluga sõlmedes (ESP32 koos PSRAM-iga). Router nodes are ideal candidates since they're typically always-on. -### Kaugustesti moodul +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Puhverdab ajutiselt võrguühenduseta olnud sõlmede sõnumeid ja esitab need uu Automatiseeritud vahemiku testimise tööriist sõlmede vahelise ühenduse kvaliteedi hindamiseks. Kui lubatud, edastab sõlm perioodiliselt testsõnumeid kasvavate loenduritega. Vastuvõtusõlm logib need sõnumid, võimaldades kõndida või minema sõita ning hiljem analüüsida, millisel kaugusel sõnumite saabumine lakkas. -| Sätted | Kirjeldus | -| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Ulatustest lubatud | Aktiveeri levi test | -| Saatja sõnumi sagedus (sekundit) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Salvesta .CSV faili (ainult ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Sätted | Kirjeldus | +| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Ulatustest lubatud | Aktiveeri levi test | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Salvesta .CSV faili (ainult ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemeetria moodul +### Telemetry module Juhib, milliseid telemeetriaandmeid sõlm võrguga jagab. Telemeetria sisaldab seadme tervist (aku, tööaeg) ja keskkonnaandurite andmeid (temperatuur, niiskus, rõhk). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Sätted | Kirjeldus | -| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Saada seadme telemeetria | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Seadme mõõdikute värskendamise intervall | How often to report battery, uptime and channel utilization | -| Keskkonnamõõdikute lubamine | Report the attached environment sensors | -| Keskkonnamõõdikute värskendamise intervall | How often to report them | -| Keskkonnamõõdikute ekraanil kuvamine lubatud | Also show these readings on the device's own display | -| Keskkonnamõõdikud kasutavad Fahrenheiti | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Õhukvaliteedi moodul on lubatud | Report particulate and CO₂ sensor data | -| Õhukvaliteedi näidikute värskendamise intervall | How often to report them | -| Toitemõõdiku moodul on lubatud | Report the per-channel voltage and current readings | -| Toitemõõdikute värskendamise intervall | How often to report them | -| Toitemõõdiku ekraanil kuvamine lubatud | Also show power readings on the device's display | +| Sätted | Kirjeldus | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Saada seadme telemeetria | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Seadme mõõdikute värskendamise intervall | How often to report battery, uptime and channel utilization | +| Keskkonnamõõdikute lubamine | Report the attached environment sensors | +| Keskkonnamõõdikute värskendamise intervall | How often to report them | +| Keskkonnamõõdikute ekraanil kuvamine lubatud | Also show these readings on the device's own display | +| Keskkonnamõõdikud kasutavad Fahrenheiti | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Õhukvaliteedi moodul on lubatud | Report particulate and CO₂ sensor data | +| Õhukvaliteedi näidikute värskendamise intervall | How often to report them | +| Toitemõõdiku moodul on lubatud | Report the per-channel voltage and current readings | +| Toitemõõdikute värskendamise intervall | How often to report them | +| Toitemõõdiku ekraanil kuvamine lubatud | Also show power readings on the device's display | Vaata [Telemeetria & Sensorid](telemetry-and-sensors) toetatud andurite ja sätete soovituste kohta. -### Eelsalvestatud sõnumi moodul +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Määra nimekiri kiirsõnumitest, mida saab edastada ilma telefoni ühendamata – ideaalne välitöödeks. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Määra nimekiri kiirsõnumitest, mida saab edastada ilma telefoni ühendamata – ideaalne välitöödeks. | Sätted | Kirjeldus | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Üles/Alla/Vali sisend lubatud | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio moodul +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. See on **eksperimentaalne** funktsioon, mis kodeerib hääle Codec2 koodeki abil väga väikesteks andmepakettideks. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. See on > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Kaugriistvara moodul +### Remote Hardware module GPIO juhtimine kärgvõrgu kaudu. Võimaldab kaugsõlmel lugeda või kirjutada GPIO sisendkontakte teisel sõlmel – kasulik releede aktiveerimiseks, lülitite lugemiseks või välise riistvara kaugjuhtimiseks. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Sätted | Kirjeldus | | ----------------------------------- | ----------------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO juhtimine kärgvõrgu kaudu. Võimaldab kaugsõlmel lugeda või kirjutada G | Luba määratlemata klemmi juurdepääs | Luba juurdepääs mis tahes GPIO sisendile (turvarisk) | | Saadaval klemmid | Kuni 4 GPIO sisendit, mida see sõlm kauglugemiseks/-kirjutamiseks avab | -### Naabriinfo moodul +### Neighbor Info module Levitab teavet otse kuuldud naabrite kohta, võimaldades kärgvõrgu topoloogia kaardistamist. Iga lubatud sõlm jagab perioodiliselt nimekirja teistest sõlmedest, mida ta kuuleb ja nende signaali kvaliteedist. -| Sätted | Kirjeldus | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| Naabruskonna teave lubatud | Aktiveeri naabrite leviring | -| Uuenduste sagedus (sekundit) | Kui tihti naabrite nimekirja levitada | -| Saada LoRa kaudu | Edasta naabriinfot ka LoRa kaudu, mitte ainult MQTT/telefoni kaudu. Vaikimisi võtit ja nime kasutavat kanalit pole saadaval | +| Sätted | Kirjeldus | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Naabruskonna teave lubatud | Aktiveeri naabrite leviring | +| GPS-i küsimise intervall | Kui tihti naabrite nimekirja levitada | +| Saada LoRa kaudu | Edasta naabriinfot ka LoRa kaudu, mitte ainult MQTT/telefoni kaudu. Vaikimisi võtit ja nime kasutavat kanalit pole saadaval | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambientvalguse moodul +### Ambient Lighting module Juhib toetatud riistvaral NeoPixeli või muid adresseeritavaid RGB LEDe. Saab kasutada visuaalsete olekuindikaatorite, märgutulede või dekoratiivsete efektide jaoks. @@ -216,49 +215,49 @@ Juhib toetatud riistvaral NeoPixeli või muid adresseeritavaid RGB LEDe. Saab ka | Pinge | LED current limit (0–31) | | Punane / Roheline / Sinine | Individuaalsete värvikanalite väärtused (0–255) | -### Tuvastusanduri moodul +### Detection Sensor module Muudab sõlme liikumis- või ukseanduri hoiatussüsteemiks. Kui GPIO sisend tuvastab oleku muutuse (liikumine tuvastatud, uks avatud), levitab sõlm kärgvõrgu kaudu hoiatusteate. -| Sätted | Kirjeldus | -| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Tuvastusandur lubatud | Aktiveeri tuvastusandur | -| GPIO klemmi jälgimine | GPIO sisend on anduriga ühendatud | -| Identifitseerimistüüp | Kuidas klemmi olek vastab tuvastussündmusele (nt aktiivne kõrge/madal, serva poolt käivitatav) | -| Kasuta INPUT_PULLUP režiimi | Enable the pin's internal pull-up resistor | -| Minimaalne edastusaeg (sekund) | Minimaalne aeg hoiatusteadete levitamisel | -| Oleku edastus (sekund) | Perioodilise oleku levitamise intervall | -| Saada kõll koos hoiatussõnumiga | Lisa märguannetesse hoiatuskella sümbol | -| Kasutajasõbralik nimi | Selle anduri kohandatud nimi | +| Sätted | Kirjeldus | +| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| Tuvastusandur lubatud | Aktiveeri tuvastusandur | +| GPIO klemmi jälgimine | GPIO sisend on anduriga ühendatud | +| Identifitseerimistüüp | Kuidas klemmi olek vastab tuvastussündmusele (nt aktiivne kõrge/madal, serva poolt käivitatav) | +| Kasuta INPUT_PULLUP režiimi | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimaalne aeg hoiatusteadete levitamisel | +| State Broadcast Interval | Perioodilise oleku levitamise intervall | +| Saada kõll koos hoiatussõnumiga | Lisa märguannetesse hoiatuskella sümbol | +| Kasutajasõbralik nimi | Selle anduri kohandatud nimi | -### Paxloenduri moodul +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Loendab lähedalasuvaid seadmeid, kuulates passiivselt sondimistaotlusi, mida telefonid ja sülearvutid võrkude skannimisel edastavad. Available only on ESP32 devices. -| Sätted | Kirjeldus | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter lubatud | Aktiveeri inimeste loendamine | -| Uuenduste sagedus (sekundit) | Kui tihti loendeid esitada | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Sätted | Kirjeldus | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter lubatud | Aktiveeri inimeste loendamine | +| GPS-i küsimise intervall | Kui tihti loendeid esitada | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Vihje:** Paxloendur on kasulik jalakäijate liikluse hindamiseks matkaradade alguses, ürituste toimumiskohtades või muudes kohtades. Arvud on ligikaudsed – üks inimene võib kaasas kanda mitut seadet. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK moodul +### TAK module Meeskonna teadlikkuse komplekti integratsioon ATAKi ja WinTAKi koostalitlusvõime tagamiseks. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. Vaata [TAK Integration](tak) täpsema seadistamise ja kasutamise kohta. @@ -279,32 +278,33 @@ Administraatori võtit jagavate sõlmede kaugkonfigureerimine: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------- | ------------------------------------------------------------------------------------------------------ | -| Määra aeg | Sends your phone's clock to the radio | -| Taaskäivita | Restarts the radio | -| Lülita välja | Powers the radio down | -| Tehasesätted | Returns every setting to its factory default | -| NodeDB lähtestamine | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------- | ---------------------------------------------------------------------------------------------- | +| Määra aeg | Sends your phone's clock to the node | +| Taaskäivita | Restarts the node | +| Lülita välja | Powers the node down | +| Tehasesätted | Returns every setting to its factory default | +| NodeDB lähtestamine | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Varunda ja taasta -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Täpsem **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Tühjenda sõlmede andmebaas -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Avab vahekaardid **Paketid** ja **Rakenduse logid** diagnostilise väljundi vaat ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Teave @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Kaug-admin tõrkeotsing +### Troubleshooting remote admin - **"Sihtsõlmelt ei ole vastust"** — sihtsõlm võib olla leviulatusest väljas, võrguühenduseta või sellel võib olla sobimatu administraatori võti. Veendu, et administraatori võti sobiks mõlemas sõlmele. - **Muudatused ei rakendu** — mõnede sätete jõustumiseks on vaja taaskäivitada. Pärast salvestamist proovi taaskäivitust. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Seotud teemad -- [Seaded — Raadio ja kasutaja](settings-radio-user) — raadio ja kasutajaprofiili põhiseaded +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Mooduli konfiguratsiooni viide](https://meshtastic.org/docs/configuration/module) — üksikasjalik mooduli dokumentatsioon aadressil meshtastic.org - [KKK](https://meshtastic.org/docs/faq/) — meshtastic.org sageli esitatavad küsimused diff --git a/docs/et-rEE/user/settings-radio-user.md b/docs/et-rEE/user/settings-radio-user.md index 1f67d3aa47..0859186fe9 100644 --- a/docs/et-rEE/user/settings-radio-user.md +++ b/docs/et-rEE/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Seaded — raadio ja kasutaja -parent: Kasutusjuhend nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - sätted - raadio-sätted @@ -13,14 +12,14 @@ aliases: # Seaded — raadio ja kasutaja -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Seaded kasutavad standardseid eelistuste juhtelemente – rippmenüüsid, lülitid ja liugurid: @@ -36,20 +35,20 @@ Seaded kasutavad standardseid eelistuste juhtelemente – rippmenüüsid, lülit On **Settings → User**. -| Sätted | Kirjeldus | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Täis nimi | Your display name (up to 39 characters) | -| Lühi nimi | 4-character abbreviated name | -| Oleku teavitus | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Ei võta sõnumeid vastu | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Litsentseeritud raadioamatöör (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Sätted | Kirjeldus | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Täis nimi | Your display name (up to 39 characters) | +| Lühi nimi | 4-character abbreviated name | +| Oleku teavitus | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Ei võta sõnumeid vastu | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Kordusülekannete režiim | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Sõlme teabe edastamise intervall | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Topeltpuudutus nupuna | Treat a double tap as a button press | Keelatud | -| Kolmekordne klõps Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Keelatud | +| Kolmekordne klõps Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Lubatud | | Südamelöögi LED | Blink the status LED periodically | Lubatud | | Ajavöönd | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Sätted | Kirjeldus | Vaikimisi | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | -| Regioon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Määramata (tuleb seadistada) | -| Eelseadistused | Speed/range tradeoff | LongFast | -| Hüpete arv | Maks uuesti saadetud hüpet | 3 | -| Saatevõimsus | Transmission power (dBm); 0 = max allowed for region | 0 (regiooni maks) | -| Tühista sagedus | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Kasuta eelseadistust | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Levitustegur | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Kodeerimiskiirus | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Ribalaius | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Sageduspesa | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Edastus lubatud | Turning this off makes the node receive-only | On | -| Töötsükli tühistamine | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Väljas | -| Keela MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok MQTTi | Allow your packets to be forwarded to MQTT by gateways | Väljas | -| RX võimendatud võimendus | Extra receive gain on SX126x radios; costs a little current | Väljas | -| PA ventilaator keelatud | Turn off the power-amplifier fan on hardware that has one | Väljas | +| Sätted | Kirjeldus | Vaikimisi | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| Regioon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Määramata (tuleb seadistada) | +| Eelseadistused | Speed/range tradeoff | LongFast | +| Hüpete arv | Maks uuesti saadetud hüpet | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (regiooni maks) | +| Tühista sagedus | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Kasuta eelseadistust | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Levitustegur | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Kodeerimiskiirus | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Ribalaius | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Sageduspesa | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Edastus lubatud | Turning this off makes the node receive-only | On | +| Töötsükli tühistamine | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Väljas | +| Keela MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok MQTTi | Allow your packets to be forwarded to MQTT by gateways | Väljas | +| RX võimendatud võimendus | Extra receive gain on SX126x radios; costs a little current | Väljas | +| PA ventilaator keelatud | Turn off the power-amplifier fan on hardware that has one | Väljas | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. Lisateabe saamiseks vaadake [regiooni seadistamise juhendit](https://meshtastic.org/docs/getting-started/initial-config) aadressil meshtastic.org. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Vihje:** **SNR-i piirväärtused** on meelega negatiivsed. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EL 868 MHz sagedusala (62,5 kHz ribalaius); võrreldav Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12,5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7,5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0,18 kbps | −20 dB | ⚠️ **Vananenud** — endiselt valitav, kuid võidakse tulevases püsivara versioonis eemaldada | | ~~Very Long Slow~~ | ~40+ km | 0,09 kbps | −20 dB | ⚠️ **Vananenud** — endiselt valitav, kuid võidakse tulevases püsivara versioonis eemaldada | > ℹ️ **Märkus:** Selles tabelis kasutatakse üldlevinud lühinimesid. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fikseeritud taristuühendused:** Kasutage **Short Turbo** või **Long Turbo** spetsiaalsete punkt-punkti ühenduste jaoks, millel on head antennid ja otsenähtavus. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Erinevate eelseadetega sõlmed ei saa suhelda isegi siis, kui neil on sama sagedus ja krüpteerimisvõti. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Kõrguse eelis (mäetipp, katus) suurendab märgatavalt efektiivset ulatust. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Ekraani sätted -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Sätted | Kirjeldus | -| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Ekraan sisse lülitatud | How long the display stays lit before sleeping | -| Karusselli intervall | How often the radio cycles between screens on its own | -| Ekraani režiim | Screen layout/density used by the firmware | -| Ekraani ühikud | Metric or Imperial on the radio's screen | -| Kasuta 12 tunni formaati | Show the radio's clock as 12-hour rather than 24-hour | -| Paks pealkiri | Draw the screen's heading text in bold | -| Keera ekraani | Rotate the display 180° for an inverted mounting | -| OLED tüüp | Auto, SSD1306, SH1106, SH1107 | -| Ärata puudutusega või liigutusega | Light the screen when the radio is tapped or moved | -| Kompassi suund | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Suund alati põhi | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Sätted | Kirjeldus | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ekraan sisse lülitatud | How long the display stays lit before sleeping | +| Karusselli intervall | How often the node cycles between screens on its own | +| Ekraani režiim | Screen layout/density used by the firmware | +| Ekraani ühikud | Metric or Imperial on the node's screen | +| Kasuta 12 tunni formaati | Show the node's clock as 12-hour rather than 24-hour | +| Paks pealkiri | Draw the screen's heading text in bold | +| Keera ekraani | Rotate the display 180° for an inverted mounting | +| OLED tüüp | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Ärata puudutusega või liigutusega | Light the screen when the node is tapped or moved | +| Kompassi suund | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Suund alati põhi | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Asukoha sätted On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Sätted | Kirjeldus | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS-režiim (riistvara) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS-i küsimise intervall | How often the radio asks its GPS for a fix | +| GPS-i küsimise intervall | How often the node asks its GPS for a fix | | Levitamise inteintervall | How often the position is shared with the mesh | | Nutikas asukoht | Broadcast based on movement rather than purely on the clock | | Nutikas intervall | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Sätted | Kirjeldus | | -------------------------------------------- | --------------------------------------------------------------- | -| Luba energiasäästurežiim | Let the radio sleep aggressively between activity | +| Luba energiasäästurežiim | Let the node sleep aggressively between activity | | Väljalülitamine voolukatkestuse korral | Power the device down after external power disappears | | Super sügava une kestus | How long the deepest sleep state lasts | -| Minimaalne ärkveloleku aeg | The shortest time the radio stays awake once woken | +| Minimaalne ärkveloleku aeg | The shortest time the node stays awake once woken | | Oota Bluetoothi ​​kestust | How long to wait for a phone to connect before sleeping | | ADC kordaja tühistamine | Turn on a manual correction for battery-voltage readings | | Asenda ADC kordistaja suhe | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Võrgu sätted -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Sätted | Kirjeldus | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Parool | Network password | | Ethernet lubatud | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Sinihamba sätted -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Sätted | Kirjeldus | | ------------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Sätted | Kirjeldus | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Avalik võti | Sinu sõlme avalik võti (kirjutuskaitstud) | | Administraatori võti | Keys permitted to administer this node remotely — up to three | -| Salajane võti | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Salajane võti | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Loo uus privaatvõti | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin kanal lubatud~~ | ⚠️ Eemaldatud — nüüd seadistatakse automaatselt, kui administraatori võti on määratud | | Jadapordi konsool | Serial console over the Stream API | -| Silumislogi API lubatud | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Hallatud režiim | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Silumislogi API lubatud | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Hallatud režiim | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Taastevõtmed | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Taasta võtmed | Kirjuta varundatud võtmed tagasi sõlme (saadaval siis, kui varukoopia on olemas) | | Kustuta taastevõtmed | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/et-rEE/user/signal-meter.md b/docs/et-rEE/user/signal-meter.md index 350cf4bcd7..196b07dbb6 100644 --- a/docs/et-rEE/user/signal-meter.md +++ b/docs/et-rEE/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: Kuidas Meshtastic signaalimõõtur töötab -parent: Kasutusjuhend nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/et-rEE/user/tak.md b/docs/et-rEE/user/tak.md index 5196abc110..71da88a4ae 100644 --- a/docs/et-rEE/user/tak.md +++ b/docs/et-rEE/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK integratsioon -parent: Kasutusjuhend nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Koostöö ATAKi ja WinTAKiga — CoT asukoha jagamine, TAK rollid ja pluginate seadistamine. aliases: - tak @@ -105,6 +104,7 @@ Kui on seadistatud: - Vestlussõnumid võivad ühendada kärgvõrgu ja TAK võrke - Asukohavärskendused liiguvad Meshtasticu ja TAKi vahel kahesuunaliselt - TAK jälgimisseadme sõlmed levitavad PLId automaatselt – nende asukohad kuvatakse ATAK kaartidel ilma ATAK poolse konfita +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/et-rEE/user/telemetry-and-sensors.md b/docs/et-rEE/user/telemetry-and-sensors.md index 069876e365..dd68e71b4e 100644 --- a/docs/et-rEE/user/telemetry-and-sensors.md +++ b/docs/et-rEE/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemeetria & Sensorid -parent: Kasutusjuhend nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Kärgvõrgu andurite andmed — toetatud keskkonna-, õhukvaliteedi- ja võimsusandurid ning konfiguratsiooni- ja vaatamisjuhendid. aliases: - sensorid @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Andur | Meetriline | Sõnumid | -| -------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1,0, PM2,5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Andur | Meetriline | Sõnumid | +| -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1,0, PM2,5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Valgus & UV | Andur | Meetriline | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Meetriline | Ühik | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiatsioon | µR/h | Card and chart | -| Kaal | kg or lb | Card only — load cells, such as a beehive scale | -| Kaugus | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Meetriline | Ühik | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiatsioon | µR/h | Card and chart | +| Kaal | kg or lb | Card only — load cells, such as a beehive scale | +| Kaugus | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Võimsusnäitajad diff --git a/docs/et-rEE/user/translate.md b/docs/et-rEE/user/translate.md index 52395b3e50..7fba4f1e9e 100644 --- a/docs/et-rEE/user/translate.md +++ b/docs/et-rEE/user/translate.md @@ -1,6 +1,5 @@ --- title: Tõlgi rakendus -parent: Kasutusjuhend nav_order: 17 last_updated: 2026-09-11 description: Kuidas rakendust ja selle dokumentatsiooni Crowdini kaudu tõlgitakse ja tõlgete panustamise juhised. diff --git a/docs/et-rEE/user/units-and-locale.md b/docs/et-rEE/user/units-and-locale.md index a6645f05a1..1b99a0110f 100644 --- a/docs/et-rEE/user/units-and-locale.md +++ b/docs/et-rEE/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Ühikud, mõõtühikud ja lokaat -parent: Kasutusjuhend nav_order: 16 last_updated: 2026-08-30 description: Kuidas rakendus vormindab temperatuuri, vahemaad, kiirust ja muid mõõtmisi vastavalt seadme lokaadile. diff --git a/docs/et-rEE/user/widget.md b/docs/et-rEE/user/widget.md index 3b124df65d..43c981a232 100644 --- a/docs/et-rEE/user/widget.md +++ b/docs/et-rEE/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Kasutaja juhis nav_order: 20 last_updated: 2026-08-30 description: Lisa Meshtastici avakuva vidin, et vaadata ühendatud raadio kohalikku statistikat ilma rakendust avamata. diff --git a/docs/fi-rFI/index.md b/docs/fi-rFI/index.md index 4cf76b20d7..d30f472940 100644 --- a/docs/fi-rFI/index.md +++ b/docs/fi-rFI/index.md @@ -1,6 +1,6 @@ --- title: Etusivu -layout: oletus +layout: default nav_order: 0 --- diff --git a/docs/fi-rFI/user/app-functions.md b/docs/fi-rFI/user/app-functions.md index 59cb80c128..13cfaad36b 100644 --- a/docs/fi-rFI/user/app-functions.md +++ b/docs/fi-rFI/user/app-functions.md @@ -1,6 +1,5 @@ --- title: Sovellustoiminnot -parent: Käyttöopas nav_order: 19 last_updated: 2026-08-30 description: Tuo mesh-ominaisuudet Android-järjestelmälle ja laitteessa toimiville tekoälyavustajille (esim. Gemini), jotta ne voivat suorittaa mesh-toimintoja ilman sovelluksen avaamista. diff --git a/docs/fi-rFI/user/connections.md b/docs/fi-rFI/user/connections.md index 73f5b35406..7890bbcf6c 100644 --- a/docs/fi-rFI/user/connections.md +++ b/docs/fi-rFI/user/connections.md @@ -1,8 +1,7 @@ --- title: Yhteydet -parent: Käyttöopas nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Yhdistä puhelin tai työpöytä Meshtastic-radioon Bluetoothin, USB:n tai TCP/IP:n kautta. aliases: - bluetooth @@ -44,7 +43,7 @@ Näytössä kerrotaan kaikki sovelluksen puolella olevat syyt, jotka estävät h | Kortti pyytää **Lähistön laitteet** -käyttöoikeutta | Käyttöoikeutta ei ole myönnetty. **Myönnä käyttöoikeus** pyytää sitä. Kun Android lakkaa kysymästä, painikkeeksi muuttuu **Avaa asetukset**. | | **Bluetooth on pois käytöstä** | Bluetooth-sovitin on poistettu käytöstä – kortti avaa Bluetooth-asetukset. | | **Bluetooth-laitteiden haku edellyttää myös sijaintipalvelujen käyttöä** | Vain Android 11:ssä ja vanhemmissa versioissa: käyttöoikeus on myönnetty, mutta järjestelmän sijaintipalvelut ovat pois käytöstä. | -| Ei korttia, tyhjä luettelo | Mikään sovelluksen puolella ei estä hakua – radio on kantaman ulkopuolella, pois käytöstä tai jo yhdistetty toiseen laitteeseen. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ Yhteyttä muodostettaessa tilailmaisin näyttää nykyisen yhteyden tilan — na ![Yhdistämisen tila](../../assets/screenshots/connections_connecting.png) -Jos laitteita ei löydy, sovellus näyttää tyhjän näkymän ohjeiden kanssa: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![Laitteita ei löytynyt](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Verkko | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Bluetoothin vianmääritys diff --git a/docs/fi-rFI/user/debug-logs.md b/docs/fi-rFI/user/debug-logs.md index be995cb176..24cb3bac4e 100644 --- a/docs/fi-rFI/user/debug-logs.md +++ b/docs/fi-rFI/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Virheenjäljityslokitiedot -parent: Käyttöopas nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Tarkastele ja vie sovelluksen omat virheenjäljityslokitiedot suoraan sovelluksesta ja liitä lokitiedot GitHub-vikaraporttiin ongelmien selvittämisen helpottamiseksi — adb:tä ei tarvita. aliases: - debug-lokitiedot @@ -48,7 +47,7 @@ Liitä tämä tiedosto GitHub-vikaraporttiisi. ## Työpöytä -Työpöytäsovelluksessa ei ole järjestelmän logcat-lokitietoa, joten **Sovelluslokit**-välilehti näyttää sen sijaan sovelluksen itse keräämät lokit. Haku, suodatus ja vienti toimivat samalla tavalla. +Työpöytäsovelluksessa ei ole järjestelmän logcat-lokitietoa, joten **Sovelluslokit**-välilehti näyttää sen sijaan sovelluksen itse keräämät lokit. Haku, suodatus ja vienti toimivat samalla tavalla. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Aiheeseen liittyvät aiheet diff --git a/docs/fi-rFI/user/desktop.md b/docs/fi-rFI/user/desktop.md index 52eaaf8e6d..510e78eeeb 100644 --- a/docs/fi-rFI/user/desktop.md +++ b/docs/fi-rFI/user/desktop.md @@ -1,6 +1,5 @@ --- title: Työpöytäsovellus -parent: Käyttöopas nav_order: 14 last_updated: 2026-09-11 description: Asenna ja käytä Meshtastic-työpöytäsovellusta Linuxilla, macOS:llä ja Windowsilla — yhteydet, ominaisuuksien yhtenevyys ja pikanäppäimet. diff --git a/docs/fi-rFI/user/discovery.md b/docs/fi-rFI/user/discovery.md index 4d669f5cfe..349089038a 100644 --- a/docs/fi-rFI/user/discovery.md +++ b/docs/fi-rFI/user/discovery.md @@ -1,8 +1,7 @@ --- title: Paikallisen mesh-verkon skannaus -parent: Käyttöopas nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Tutki mesh-verkkoasi — paikallinen verkon haku, reitinselvitykset, naapurikartat ja radion hakuun liittyvät työkalut. aliases: - haku @@ -20,53 +19,53 @@ Hakutyökalut auttavat ymmärtämään **miten** mesh-verkko on yhteydessä — Sovellus tarjoaa kaksi toisiaan täydentävää lähestymistapaa: -- Paikallinen verkon haku (Scanner) — automaattinen tila, joka kierrättää yhdistettyä radiota eri LoRa-esiasetusten läpi, kuuntelee jokaisella ja arvioi, mikä esiasetus toimii parhaiten sijainnissasi. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - Manuaalinen etsiminen — reitinselvityksen reitit, naapuritiedot ja radiolista, joita voit käyttää milloin tahansa yksittäisten reittien ja topologian tarkasteluun. ## Paikallinen verkon haku (Scanner) -Paikallinen verkon haku on erillinen skannaustila, joka auttaa löytämään parhaan LoRa-modeemiesiasetuksen sijaintiisi ja näkemään, mitkä radiot ovat aktiivisia kullakin esiasetuksella. Käy yhdistetyn radiosi läpi yhdellä tai useammalla valitsemallasi esiasetuksella, viipyy jokaisella määritetyn ajan paketteja keräten ja analysoi lopuksi tulokset sekä asettaa ne paremmuusjärjestykseen. +Paikallinen verkon haku on erillinen skannaustila, joka auttaa löytämään parhaan LoRa-modeemiesiasetuksen sijaintiisi ja näkemään, mitkä radiot ovat aktiivisia kullakin esiasetuksella. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Huomautus:** Discovery muuttaa väliaikaisesti radiosi LoRa-asetuksia skannauksen ajaksi ja palauttaa alkuperäiset asetukset, kun skannaus on valmis. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Skannauksen asetukset +### Setting up a scan Ennen aloittamista määritä nämä asetukset: -| Säädin | Kuvaus | -| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **LoRa-esiasetuksen valitsin** | Valitse yksi tai useampi esiasetus skannattavaksi. Haku pysähtyy jokaisessa valitussa esiasetuksessa vuorollaan kuuntelemaan liikennettä. | -| **Kuunteluaika** | Kunkin esiasetuksen kuunteluaika. Valitse 1, 5, 15, 30, 45, 60, 90, 120 tai 180 minuuttia. Pidempi kuunteluaika kerää enemmän paketteja ja antaa tarkemman kuvan, mutta kestää pidempään. | -| **Pidä näyttö päällä** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Säädin | Kuvaus | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa-esiasetuksen valitsin** | Valitse yksi tai useampi esiasetus skannattavaksi. Haku pysähtyy jokaisessa valitussa esiasetuksessa vuorollaan kuuntelemaan liikennettä. | +| **Kuunteluaika** | Kunkin esiasetuksen kuunteluaika. Valitse 1, 5, 15, 30, 45, 60, 90, 120 tai 180 minuuttia. Pidempi kuunteluaika kerää enemmän paketteja ja antaa tarkemman kuvan, mutta kestää pidempään. | +| **Pidä näyttö päällä** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Yleisiä syitä, miksi se on pois käytöstä: -- Radio **ei ole yhdistetty**. +- The node is **not connected**. - **Esiasetuksia ei ole valittu** skannattavaksi. - Valittu esiasetus käyttää **2,4 GHz -taajuutta**, jota laitteistosi ei tue. -### Reaaliaikainen edistyminen +### Live progress Skannauksen aikana haku näyttää sen nykyisen vaiheen: -| Tila | Mitä tapahtuu | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Tallennetaan nykyinen kokoonpano ja valmistaudutaan skannaukseen. | -| **Vaihdetaan kohteeseen ** | Vaihdetaan radio seuraavaan esiasetukseen testausta varten. | -| **Reconnecting on \** | Yhteys muodostetaan uudelleen esiasetuksen vaihdon jälkeen. | -| **Dwelling on \** | Nykyisen esiasetuksen kuuntelu pakettien keräämiseksi, seuraavaan vaiheeseen siirtymisen laskuri käynnissä. | -| **Analyzing results** | Kerättyjen pakettien käsittely ja esiasetusten vertailu ja pisteytys. | -| **Restoring home preset** | Palautetaan alkuperäinen LoRa-konfiguraatio takaisin. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Tila | Mitä tapahtuu | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Tallennetaan nykyinen kokoonpano ja valmistaudutaan skannaukseen. | +| **Vaihdetaan kohteeseen ** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Yhteys muodostetaan uudelleen esiasetuksen vaihdon jälkeen. | +| **Dwelling on \** | Nykyisen esiasetuksen kuuntelu pakettien keräämiseksi, seuraavaan vaiheeseen siirtymisen laskuri käynnissä. | +| **Analyzing results** | Kerättyjen pakettien käsittely ja esiasetusten vertailu ja pisteytys. | +| **Restoring home preset** | Palautetaan alkuperäinen LoRa-konfiguraatio takaisin. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Kuuntelun laskuri näyttää jäljellä olevan ajan nykyisessä esiasetuksessa](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Tulosten lukeminen +### Reading the results Kun skannaus valmistuu, haku näyttää jokaiselle testatulle esiasetukselle oman tuloskortin sekä yleisen yhteenvedon. @@ -94,28 +93,28 @@ Tuloksista saatavilla olevat lisätoiminnot: Mesh Beacon antaa radioille mahdollisuuden kutsua muita liittymään mesh-verkkoon. Majakkatilassa oleva radio lähettää säännöllisesti kutsun, jossa voidaan haluttaessa ilmoittaa kanava, alue ja modeemiesiasetus — lähellä olevat radiot voivat havaita sen jo ennen asetusten jakamista. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Kuuntele majakoita** – vastaanota muiden radioiden lähettämiä liittymiskutsuja. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Vastaanotetut kutsut näkyvät **Mesh-kutsut** -korteissa haku-näytössä. Jokainen kortti näyttää lähettäjän viestin sekä tarjotun kanavan, alueen, esiasetuksen ja signaalin laadun. Käytettävissä ovat seuraavat toiminnot: -- **Liity** – vaihda tarjottuun kanavaan ja esiasetukseen (radio palautetaan alkutilaan ja käynnistyy uudelleen). Jos tarjous vastaa nykyistä taajuuspaikkaasi, **Lisää kanava** lisää sen ilman uudelleenkäynnistystä. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). Jos tarjous vastaa nykyistä taajuuspaikkaasi, **Lisää kanava** lisää sen ilman uudelleenkäynnistystä. - **Tutki** – aloita Haku tarjotulla esiasetuksella, jotta voit tutkia mesh-verkkoa ennen liittymistä (näkyy vain, jos majakka tarjoaa esiasetuksen). - **Hylkää** – ohita kutsu. Majakoiden ilmoittamat kanavat näkyvät myös Haku-toiminnon asetuksissa kohdassa **Majakkakanavat**. Valitse kanava lisätäksesi sen hakukohteeksi. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manuaalinen haku +## Manual exploration Seuraavat työkalut ovat aina käytettävissä Radiot- ja Radion tiedot -näkymissä. Käytä niitä yksittäisten reittien tutkimiseen ja topologian muodostamiseen, joko osana skannausta tai sen sijaan. @@ -128,7 +127,7 @@ Reitinselvitys näyttää tarkan polun, jota kautta viesti kulkee omalta radiolt 1. Siirry kohtaan **Radiot** ja napauta radiota, jota haluat jäljittää. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Tulosten lukeminen +#### Reading the results Reitinselvityksen tulos näyttää tältä: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| Mitä kannattaa tarkkailla | Mitä se tarkoittaa | -| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| Kaikki hypyt näyttävät hyvän SNR:n (≥ −7 dB, vihreä) | Hyvä reitti — viestit kulkevat luotettavasti | -| One hop shows a poor SNR (below −15 dB, orange) | Heikko yhteys — tämä välityssegmentti on haavoittuva | -| Useita hyppyjä (4+) | Pitkä reitti — harkitse radion siirtämistä sen lyhentämiseksi | -| Eri reitti uudelleenyrityksellä | Verkko mukautuu — useita reittejä on olemassa (tämä on hyvä!) | +| Mitä kannattaa tarkkailla | Mitä se tarkoittaa | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Hyvä reitti — viestit kulkevat luotettavasti | +| One hop shows a poor SNR (orange or red) | Heikko yhteys — tämä välityssegmentti on haavoittuva | +| Useita hyppyjä (4+) | Pitkä reitti — harkitse radion siirtämistä sen lyhentämiseksi | +| Eri reitti uudelleenyrityksellä | Verkko mukautuu — useita reittejä on olemassa (tämä on hyvä!) | > Vinkki: Aja reitinselvitys useita kertoja muutaman minuutin aikana. Jos reitti muuttuu, verkossasi on varareittejä — merkki hyvin kytketystä verkosta. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Varmista, että molemmat radiot jakavat vähintään yhden kanavan samalla salausavaimella. - **Reitinselvitys aikakatkaistu** — reitti voi olla liian pitkä (ylittää hyppymäärärajan) tai välittäjä-radio on ruuhkautunut. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Epäsymmetriset reitit** — reitinselvitys A→B voi kulkea eri reittiä kuin B→A. Tämä on normaalia — radiosignaalin eteneminen ei aina ole symmetristä. +- **Epäsymmetriset reitit** — reitinselvitys A→B voi kulkea eri reittiä kuin B→A. This is normal — radio propagation isn't always symmetric. ### Naapuritieto @@ -168,42 +167,42 @@ Naapuritieto-moduuli antaa jokaisen radion lähettää listan radioista, jotka s 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Ota moduuli käyttöön. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Myös muut radiot, joissa naapuritieto on käytössä, toimivat samalla tavalla. -#### Naapuritiedon katselu +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Jokainen naapurimerkintä näyttää radion, joka on kuultu suoraan, sekä sen signaalilaadun. - Yhdistä naapuritiedot useista radioista ymmärtääksesi koko mesh-verkon topologian. -> ℹ️ **Huomautus:** Naapuritieto lisää lähetysaikaa, koska jokainen toiminnon ottanut radio lähettää säännöllisesti naapuriluettelonsa. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Huomautus:** Naapuritieto lisää lähetysaikaa, koska jokainen toiminnon ottanut radio lähettää säännöllisesti naapuriluettelonsa. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Radiolista hakutyökaluna +### Node list as a discovery tool Radiolista on itsessään tehokas hakutyökalu, kun käytät sen suodatus- ja lajitteluominaisuuksia oikein. -#### Uusien radioiden löytäminen +#### Finding new nodes - Lajittele **Viimeksi kuultu** nähdäksesi viimeksi aktiiviset radiot ylimpänä. -- Ota käyttöön **Sisällytä tuntemattomat**, jos haluat nähdä radiot, jotka ovat ilmestyneet mesh-verkkoon mutta eivät ole vielä lähettäneet käyttäjätietojaan — nämä ovat usein juuri käynnistettyjä radioita. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Yhteyksien arviointi +#### Assessing connectivity - Lajittele **Hyppyjen määrä** nähdäksesi mitkä radiot ovat suoraan tavoitettavissa (0 hyppyä) ja mitkä välitettyinä. - Lajittele **Etäisyys** löytääksesi lähellä olevat radiot ja varmistaaksesi niiden tavoitettavuuden. -- Käytä **Rajaa MQTT pois** keskittyäksesi radioyhteyksillä tavoitettaviin radioihin (ei internet-sillan kautta). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastruktuurin tarkistus +#### Infrastructure audit - Poista **Jätä infrastruktuuri pois** käytöstä, jos haluat nähdä Router-, Router Late- ja Client Base -radiot. - Tarkista niiden signaalin laatu ja viimeksi kuultu -ajat varmistaaksesi, että infrastruktuuriradiot ovat kunnossa. Katso [Radiot](nodes) saadaksesi lisätietoa suodatus- ja lajitteluasetuksista. -## Vinkkejä mesh-verkon tutkimiseen +## Tips for Mesh exploration - Aloita reitinselvityksellä — se antaa välittömän ja käytännöllisen tiedon yksittäisestä reitistä. - Ota naapuritieto käyttöön keskeisissä radioissa — erityisesti reitittimissä ja toistimissa, jotta saat näkyvyyden runkoverkkoon. diff --git a/docs/fi-rFI/user/firmware.md b/docs/fi-rFI/user/firmware.md index d41147aaf8..d5e02927a9 100644 --- a/docs/fi-rFI/user/firmware.md +++ b/docs/fi-rFI/user/firmware.md @@ -1,6 +1,5 @@ --- title: Laiteohjelmiston päivitykset -parent: Käyttöopas nav_order: 13 last_updated: 2026-09-06 description: Päivitä radiosi laiteohjelmisto bluetoothin tai USB:n kautta — OTA-päivitys, versiokanavat, tarkistukset ennen päivitystä ja palautus. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as Sovellus lukee valitsemaltasi asemalta tiedoston `INFO_UF2.TXT` varmistaakseen, että kyseessä on todella laitteen päivitysasema, sekä tunnistaakseen laitteen ennen kuin mitään kirjoitetaan. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. Web Flasherissa väärän Bluetooth-pinon valitseminen voi johtaa siihen, että radion palauttaminen onnistuu vain laitteisto-ohjelmoijan avulla. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/fi-rFI/user/help-and-docs.md b/docs/fi-rFI/user/help-and-docs.md index 329e155d58..e2d6479075 100644 --- a/docs/fi-rFI/user/help-and-docs.md +++ b/docs/fi-rFI/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Ohjeet jasovelluksen sisäinen dokumentaatio -parent: Käyttöopas nav_order: 21 last_updated: 2026-09-11 description: Selaa tätä dokumentaatiota sovelluksessa, hae siitä tietoa ja kysy Meshtasticiin liittyviä kysymyksiä Chirpyltä — laitteella toimivalta tekoälyavustajalta. diff --git a/docs/fi-rFI/user/map-and-waypoints.md b/docs/fi-rFI/user/map-and-waypoints.md index e4072cfca6..a7a036a8ff 100644 --- a/docs/fi-rFI/user/map-and-waypoints.md +++ b/docs/fi-rFI/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Kartta ja reittipisteet -parent: Käyttöopas nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Näytä radioiden sijainnit kartalla, luo ja jaa reittipisteitä, hallitse karttatasoja ja Site Planneria sekä säädä sijainnin jakamista ja tietosuoja-asetuksia. aliases: - kartta @@ -102,7 +101,7 @@ Koska reittipisteet (ja niiden aluerajaukset) lähetetään koko mesh-verkkoon, ## Karttatasot -Napauta kartan tasokuvaketta avataksesi **Hallitse karttatasoja**. Tuo omia peitekuvia `.kml`-, `.kmz`- tai GeoJSON-muodossa, mukaan lukien KMZ-maanpeitekuvat (georeferoidut kuvat, kuten viedyt topografiset tai ilmakuvat), jotka sijoitetaan kartalle niiden määritettyjen rajojen mukaisesti. Lisää sellainen valitsemalla tiedosto **Lisää taso** -toiminnolla, avaamalla tiedosto Meshtasticissa tai jakamalla se sovellukseen toisesta sovelluksesta. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Napauta kartan tasokuvaketta avataksesi **Hallitse karttatasoja**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Lisää sellainen valitsemalla tiedosto **Lisää taso** -toiminnolla, avaamalla tiedosto Meshtasticissa tai jakamalla se sovellukseen toisesta sovelluksesta. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Tuodut karttatasot näkyvät luettelossa, jossa voit näyttää tai piilottaa ne sekä poistaa ne. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. Tämä toimii Google Play -versiossa, F-Droid-versiossa ja **Desktopissa**, joissa käytetään samaa karttatasojen tallennusta ja tiedostonvalitsinta. @@ -159,6 +158,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Karttatiilet tallennetaan välimuistiin levylle, joten kartan siirtäminen ei lataa juuri katsottua aluetta uudelleen. **Androidissa** tällä samalla näkymällä voidaan tuoda myös paikallinen `.mbtiles`-arkisto täysin offline-käyttöä varten. diff --git a/docs/fi-rFI/user/messages-and-channels.md b/docs/fi-rFI/user/messages-and-channels.md index 63975a9c09..e964e1fe31 100644 --- a/docs/fi-rFI/user/messages-and-channels.md +++ b/docs/fi-rFI/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Viestit ja kanavat -parent: Käyttöopas nav_order: 3 last_updated: 2026-09-14 description: Lähetä ja vastaanota viestejä, hallitse kanavia, määritä salaus, hae keskusteluja sekä käytä pikachatia, reaktioita ja viestitoimintoja. diff --git a/docs/fi-rFI/user/mqtt.md b/docs/fi-rFI/user/mqtt.md index 6b0a3ce8b2..79e8f8ce5d 100644 --- a/docs/fi-rFI/user/mqtt.md +++ b/docs/fi-rFI/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: Käyttöopas nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Siltaa mesh-verkko internetiin — MQTT-välityspalvelimen käyttöönotto, salauskerrokset ja karttadatan välitys. aliases: - mqtt @@ -49,31 +48,29 @@ Internet-yhteydellä varustettu yhdyskäytäväradio (WiFi tai Ethernet) julkais | **JSON output enabled** | Julkaise ja vastaanota myös `/2/json/`-aihetta. Merkitty protobuf-rakenteessa vanhentuneeksi, mutta tämä on edelleen ainoa asetus tähän toimintaan — ja sovelluksen oma välityspalvelin käyttää sitä | Ei käytössä | | **TLS enabled** | Yhteyden suojaaminen välityspalvelimeen | Ei käytössä | | **Map reporting** | Sijainnin julkaisu julkiselle kartalle | Ei käytössä | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Ei käytössä | +| **Proxy to client enabled** | Relay MQTT through the connected app | Ei käytössä | ### Yhteyden tila ja testaa yhteys -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. **Testaa yhteys** tarkistaa välityspalvelimen ennen asetusten tallentamista radioon ja erottaa eri virhetilanteet: palvelinnimen selvitys epäonnistui, TCP-yhteys hylättiin, TLS epäonnistui, yritys aikakatkaistiin tai välityspalvelin hylkäsi tunnistetietosi syyn kera. -### MQTT-välityspalvelin tässä puhelimessa +### MQTT Proxy in This App -Jos radiollasi ei ole omaa internetyhteyttä, se voi käyttää yhdistettyä puhelinta MQTT-yhdyskäytävänään: ota moduulin asetuksista käyttöön **MQTT** ja **Välityspalvelin käytössä**, jolloin sovellus välittää MQTT-liikenteen radion ja välityspalvelimen välillä puhelimesi internetyhteyden kautta. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Huomautus:** MQTT-välitys toimii vain mobiilisovelluksessa. Työpöytäsovelluksessa MQTT-asetukset ovat käytettävissä, mutta niiden taustalla ei ole välityspalvelua. - -MQTT-asetusten yläreunassa oleva **MQTT-välityspalvelin tällä puhelimella** -kytkin näyttää, onko tämä välitys käytössä, ja sen avulla voit pysäyttää sen (tai käynnistää sen uudelleen) heti ilman, että radion MQTT-asetuksia tarvitsee muokata tai tallentaa uudelleen. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Oletus Meshtastic-välityspalvelin Yhteisö ylläpitää julkista välityspalvelinta osoitteessa `mqtt.meshtastic.org`. Tämä on tarkoitettu yleiseen käyttöön ja testaukseen. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Tietosuoja:** Julkisen välityspalvelimen viestit ovat kaikkien tilaajien luettavissa. Käytä aina kanavasalausta yksityiseen viestintään. @@ -91,7 +88,7 @@ Määritä radiosi osoittamaan omaan välityspalvelimeesi oikeilla tunnistetiedo When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/fi-rFI/user/node-metrics.md b/docs/fi-rFI/user/node-metrics.md index 1a4ccc5ee3..e4bebfe9e7 100644 --- a/docs/fi-rFI/user/node-metrics.md +++ b/docs/fi-rFI/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Radion mittarit -parent: Käyttöopas nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetrianäkymät jokaiselle verkon radiolle — laitteen kunto, ympäristöanturit, ilmanlaatu, signaalin laatu, virta, reitinselvitys ja sijaintihistoria. aliases: - mittarit @@ -152,7 +151,7 @@ Reitinselvitys näyttää viestin kulkeman reitin verkossa: ### Reitinselvityksen tulosten lukeminen -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/fi-rFI/user/nodes.md b/docs/fi-rFI/user/nodes.md index 814e8ab1df..e69d81a0f3 100644 --- a/docs/fi-rFI/user/nodes.md +++ b/docs/fi-rFI/user/nodes.md @@ -1,8 +1,7 @@ --- title: Laitteet -parent: Käyttöopas nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Selaa, suodata ja lajittele verkon radioita — tarkastele tietoja, signaalin laatua, rooleja ja pikatoimintoja. aliases: - radiolista @@ -15,32 +14,36 @@ aliases: Radionäkymässä luetellaan kaikki mesh-verkossasi näkyvät radiot. -## Radiolista +## Node list -Radioluettelo näyttää kaikki radiot, joista radiosi on vastaanottanut tietoja, mukaan lukien: +The node list shows every node your node has heard, including: - **Radion nimi** — käyttäjän määrittämä pitkä nimi - **Lyhyt nimi** — 4-merkkinen tunniste -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Viimeksi kuultu** — aika viimeisimmästä yhteydestä - **Etäisyys** — arvioitu etäisyys (jos sijaintitiedot jaetaan) - **Akku** — etäradion akun varaustaso (jos telemetria on käytössä) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Radion tilailmaisimet +### Node Status indicators -| Ilmaisin | Tarkoitus | -| --------------------- | ------------------------------------------------------- | -| Green last-heard time | Radio kuultu viimeisen 2 tunnin aikana | -| Plain last-heard time | Radiosta ei ole kuultu yli 2 tuntiin | -| ⭐ Suosikki | Radio, jonka olet merkinnyt suosikiksi. | +| Ilmaisin | Tarkoitus | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Radio kuultu viimeisen 2 tunnin aikana | +| Plain last-heard time | Radiosta ei ole kuultu yli 2 tuntiin | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Suosikki | Radio, jonka olet merkinnyt suosikiksi. | -Erillistä "poissa"-tilaa ei ole. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Radion roolit +### Radioiden roolit Radioille voidaan määrittää erilaisia rooleja, jotka vaikuttavat niiden toimintaan verkossa: @@ -58,18 +61,18 @@ Radioille voidaan määrittää erilaisia rooleja, jotka vaikuttavat niiden toim | Sensor | Optimoitu telemetrian raportointiin | | TAK | Yhteensopiva TAK-järjestelmien kanssa (lähettää ja vastaanottaa CoT-viestejä) | | TAK Tracker | Vain TAK-sijainnin raportointi | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Roolin valitseminen +### Choosing a role Useimpien käyttäjien kannattaa käyttää oletusarvoista **Client**-roolia. Harkitse muuta roolia seuraavissa tilanteissa: -- **Router** — Sinulla on radio kiinteässä, korkealla sijaitsevassa paikassa, jossa on luotettava virransyöttö (katto, mäki). Routerit pysyvät jatkuvasti hereillä välittääkseen muiden viestejä ja ovat tärkeitä verkon peittoalueen laajentamisessa. Älä käytä Reititin-roolia akkukäyttöisissä käsiradioissa. +- **Router** — Sinulla on radio kiinteässä, korkealla sijaitsevassa paikassa, jossa on luotettava virransyöttö (katto, mäki). Routerit pysyvät jatkuvasti hereillä välittääkseen muiden viestejä ja ovat tärkeitä verkon peittoalueen laajentamisessa. Don't use Router on battery-powered handheld nodes. - **Router Late** — Infrastruktuuriradio, joka lähettää paketit uudelleen kerran, mutta vasta kaikkien muiden reititystilojen jälkeen. Tarjoaa lisäpeittoa paikallisille ryhmille kilpailematta ensisijaisten Routerien kanssa. - **Client Base** — Käsittelee suosikkiradioihisi menevän tai niistä tulevan liikenteen Router Late -prioriteetilla (varmistaen näille viesteille ylimääräisen välityspeiton), samalla kun kaikki muu käsitellään tavallisen Client-roolin tavoin. -- **Client Mute** — Voit vastaanottaa verkkoliikennettä, mutta et osallistu viestien välittämiseen. Hyödyllinen vain kuunteluun tarkoitetuissa radioissa tai ruuhkan vähentämiseen tiheillä alueilla. -- **Tracker** "seurantalaite" — miehittämätön radio, jonka ainoa tehtävä on lähettää GPS-sijaintiaan (esimerkiksi ajoneuvo, henkilö tai muu kohde). Nukkuu lähetysten välillä akun säästämiseksi. -- **Anturi** — miehittämätön radio, joka lähettää ympäristötelemetriaa (lämpötila, ilmankosteus, ilmanlaatu). Samanlainen virrankulutusprofiili kuin Tracker-roolissa. +- **Client Mute** — Voit vastaanottaa verkkoliikennettä, mutta et osallistu viestien välittämiseen. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Nukkuu lähetysten välillä akun säästämiseksi. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Samanlainen virrankulutusprofiili kuin Tracker-roolissa. - **TAK / TAK Tracker** — Tarvitaan vain yhteensopivuuteen ATAK-/WinTAK-järjestelmien kanssa. Katso [TAK-integraatio](tak) lisätietoja varten. > 💡 **Vinkki:** Verkko toimii parhaiten, kun suurin osa radioista käyttää **Client**- tai **Router**-roolia. Liian suuri määrä Client Mute (mykistetty) -radioita heikentää mesh-verkon vikasietoisuutta. Liian useat Router -roolin radiot tiheällä alueella voivat aiheuttaa ruuhkaa. Hyvä nyrkkisääntö on yksi Router jokaista 5–10 Client-roolia kohden alueellasi. @@ -78,19 +81,19 @@ Useimpien käyttäjien kannattaa käyttää oletusarvoista **Client**-roolia. Ha Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Kuvake | Merkitys | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Ei täsmää | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Kuvake | Merkitys | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Ei täsmää | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Pikatoiminnot @@ -107,10 +110,10 @@ Radioluettelosta voit: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Suodatus ja lajittelu +## Filtering & sorting -### Tekstihaku +### Text search Kirjoita hakukenttään suodattaaksesi radioita nimen tai lyhyen nimen perusteella. Suodatus päivittyy reaaliajassa kirjoittaessasi. -### Suodatusvalinnat +### Filter toggles -| Suodatus | Kuvaus | -| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Näytä vain radiot, joista on kuultu viimeisten 2 tunnin aikana | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Näytä tuntemattomat** | Näytä radiot, jotka eivät ole vielä lähettäneet käyttäjätietoja. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Ohita infrastruktuurilaitteet** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Rajaa MQTT pois** | Piilottaa radiot, joista on kuultu vain MQTT-internetsillan kautta | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Suodatus | Kuvaus | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Näytä vain radiot, joista on kuultu viimeisten 2 tunnin aikana | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Näytä tuntemattomat** | Näytä radiot, jotka eivät ole vielä lähettäneet käyttäjätietoja. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Ohita infrastruktuurilaitteet** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Rajaa MQTT pois** | Piilottaa radiot, joista on kuultu vain MQTT-internetsillan kautta | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Lajitteluvaihtoehdot +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Lajittelu | Kuvaus | | --------------------------------------------- | --------------------------------------------------------------------------------- | @@ -146,14 +167,14 @@ Kirjoita hakukenttään suodattaaksesi radioita nimen tai lyhyen nimen perusteel | **Etäisyys** | Lähimpänä olevat radiot ensin (edellyttää sijainnin jakamista) | | **Hyppyjä** | Vähiten välityshyppyjä vaativat radiot ensin | | **Kanava** | Ryhmitelty kanavaindeksin mukaan | -| **via MQTT** | Ryhmitelty MQTT:n kautta kuultuihin ja radiolla kuultuihin | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Radiot hyppymäärän mukaan +## Nodes per hop -Avaa pylväskaavio, joka näyttää radioiden määrän kullakin hyppyetäisyydellä, napauttamalla radioluettelon sovelluspalkissa olevaa hyppyhistogrammikuvaketta (0 = suora yhteys, 1 = yksi välityshyppy ja niin edelleen). Suodata kaavio **Viimeksi kuultu** -ajanjakson mukaan — Kaikki ajat, 1 tunti, 8 tuntia tai 24 tuntia — nähdäksesi, miltä mesh-verkko näyttää juuri nyt verrattuna pidempään ajanjaksoon. Tämä on nopea tapa arvioida, kuinka laaja ja kuormittunut paikallinen mesh-verkkosi on. +Avaa pylväskaavio, joka näyttää radioiden määrän kullakin hyppyetäisyydellä, napauttamalla radioluettelon sovelluspalkissa olevaa hyppyhistogrammikuvaketta (0 = suora yhteys, 1 = yksi välityshyppy ja niin edelleen). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. Tämä on nopea tapa arvioida, kuinka laaja ja kuormittunut paikallinen mesh-verkkosi on. -## Radion tiedot +## Node detail Radion napauttaminen avaa tietonäkymän, jossa on kattavat tiedot. Katso [Radion mittarit](node-metrics) saadaksesi täydelliset tiedot mittareista ja telemetriasta. @@ -173,7 +194,19 @@ Rivinsisäiset tilailmaisimet näyttävät tärkeimmät tiedot yhdellä silmäyk | Viimeksi kuultu | ![Viimeksi kuultu](../../assets/screenshots/nodes_last_heard.png) | | Etäisyys | ![Etäisyys](../../assets/screenshots/nodes_distance_info.png) | -### Laite-linkit ("Haluan sellaisen") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Merkitys | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") Kun radion laitteisto tunnistetaan, tietonäkymä näyttää avattavan **"Haluan sellaisen"** -osion, jossa on linkkejä laitteen ostamiseen tai lisätietojen hankkimiseen: valmistajan tuotesivu, tuoteversiot sekä alueelliset kauppapaikkalistaukset (esim. AliExpress, Amazon ja tuetut jälleenmyyjät), suodatettuna maasi mukaan. Jokainen linkki avautuu mesh.to -uudelleenohjauspalvelun kautta. Laitteet, joille ei löydy vastaavia linkkejä, eivät näytä tätä osiota. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Aiheeseen liittyvät aiheet diff --git a/docs/fi-rFI/user/notifications.md b/docs/fi-rFI/user/notifications.md new file mode 100644 index 0000000000..77add08531 --- /dev/null +++ b/docs/fi-rFI/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Ilmoitukset +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Ilmoitukset + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------- | +| Viestit | Suorien viestien ilmoitukset | A message sent directly to you | The conversation | +| Viestit | Yleislähetysviestien ilmoitukset | A message on one of your channels | The channel | +| Viestit | Reittipisteilmoitukset | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Viestit | Hälytysilmoitukset | A critical alert from a node | The conversation | +| Verkko | Uuden laitteen ilmoitukset | A node heard for the first time | The node's details | +| Verkko | Mesh-verkon ilmoitukset | An invitation to join a nearby mesh | Paikallisen mesh-verkon skannaus | +| Verkko | Akun vähäisen varauksen ilmoitukset (suosikkilaitteet) | A favorite node's battery running low | The node's details | +| Laite | Palveluilmoitukset | The connection to your node while the app runs in the background | The app | +| Laite | Akun vähäisen varauksen ilmoitukset | Your node's battery running low | The node's details | +| Laite | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Laite | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Aiheeseen liittyvät aiheet + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/fi-rFI/user/onboarding.md b/docs/fi-rFI/user/onboarding.md index b06f40a6ce..0988d4b002 100644 --- a/docs/fi-rFI/user/onboarding.md +++ b/docs/fi-rFI/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Aloittaminen -parent: Käyttöopas nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Ensimmäisen käynnistyksen määritys — käyttöoikeudet, käyttöönottoprosessi ja seuraavat vaiheet radion yhdistämisen jälkeen. aliases: - ensimmäinen käynnistys @@ -55,6 +54,8 @@ Meshtastic käyttää sijaintiasi myös seuraaviin tarkoituksiin: - Etäisyyksien laskeminen muihin radioihin - GPS-koordinaattiesi jakaminen muiden verkon jäsenten kanssa (jos käytössä) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Myönnä **Sovelluksen käytön aikana**. Sovellus ei pyydä taustasijaintia — sen manifestissa ei ole ACCESS_BACKGROUND_LOCATION-oikeutta — joten Android ei tarjoa **Aina**-vaihtoehtoa, ja sijaintipäivitykset tapahtuvat sovelluksen ollessa etualalla tai sen suorittaessa taustapalvelua. Sijaintikäyttöoikeuden hylkääminen ei estä sovelluksen muuta toimintaa. Android 12:ssa ja uudemmissa versioissa Bluetooth toimii edelleen, ja vain kartta, sijainnin näyttäminen ja sijainnin jakaminen poistuvat käytöstä. Android 11:ssä ja uudemmissa versioissa myös Bluetooth-skannaus estyy, koska Android liittää sen tähän käyttöoikeuteen — lisäksi järjestelmän **Sijaintipalvelut** on oltava käytössä, jotta skannaus palauttaa tuloksia. diff --git a/docs/fi-rFI/user/settings-module-admin.md b/docs/fi-rFI/user/settings-module-admin.md index f188c1bd1c..9d82274a39 100644 --- a/docs/fi-rFI/user/settings-module-admin.md +++ b/docs/fi-rFI/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Asetukset — Moduulit ja ylläpito -parent: Käyttöopas nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Määritä valinnaiset ominaisuusmoduulit (MQTT, telemetria, valmiit viestit, TAK ja muut) sekä suorita laitteen ylläpitotoimia. aliases: - moduulit @@ -14,7 +13,7 @@ aliases: Määritä valinnaiset ominaisuusmoduulit ja suorita laitteen ylläpitotoimia. Moduulit laajentavat Meshtasticia erikoisominaisuuksilla — jokainen voidaan ottaa käyttöön tai poistaa käytöstä erikseen. -> 💡 **Vinkki:** Ota käyttöön vain ne moduulit, joita todella käytät. Käyttämättömien moduulien poistaminen käytöstä vähentää lähetyksen käyttöastetta, säästää akkua ja yksinkertaistaa määrityksiä. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Vinkki:** Ota käyttöön vain ne moduulit, joita todella käytät. Käyttämättömien moduulien poistaminen käytöstä vähentää lähetyksen käyttöastetta, säästää akkua ja yksinkertaistaa määrityksiä. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Moduuliasetukset käyttävät korttipohjaista asettelua, jossa on kytkimiä, pudotusvalikoita, tekstikenttiä ja liukusäätimiä: @@ -26,15 +25,15 @@ Moduuliasetukset käyttävät korttipohjaista asettelua, jossa on kytkimiä, pud ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Moduulin määritys +## Moduulin asetukset Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT-moduuli +### MQTT module -Yhdistää verkon viestejä MQTT-välityspalvelimeen ja sieltä takaisin internet-yhteyksiä varten. Näin laajennat verkkoasi radiokantaman ulkopuolelle tai integroit sen kodin automaatiojärjestelmiin. +Yhdistää verkon viestejä MQTT-välityspalvelimeen ja sieltä takaisin internet-yhteyksiä varten. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Asetus | Kuvaus | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Yhdistää verkon viestejä MQTT-välityspalvelimeen ja sieltä takaisin interne | JSON ulostulo käytössä | Julkaise ja vastaanota MQTT-viestejä JSON-muodossa. Merkitty protobuf-rakenteessa vanhentuneeksi, mutta tämä on edelleen ainoa asetus tähän toimintaan, ja laiteohjelmisto käyttää sitä yhä | | TLS käytössä | Käytä suojattua yhteyttä | | Palvelimen osoite (root topic) | MQTT:n perusaihepolku | -| Välityspalvelin käytössä | Anna yhdistetyn puhelimen välittää radion MQTT-liikenne sen sijaan, että radio muodostaisi itse yhteyden välityspalvelimeen | -| MQTT-välityspalvelin tällä puhelimella | Yllä olevan **Välitys asiakkaalle käytössä** -asetuksen puhelinpään osuus: käyttääkö tämä puhelin kyseistä välitystä. Katso [MQTT](mqtt) | +| Välityspalvelin käytössä | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. Katso [MQTT](mqtt) | | Karttaraportointi | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Asetus | Kuvaus | -| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Hyväksyn. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Karttaraportoinnin aikaväli (sekuntia) | Kuinka usein sijainti julkaistaan. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Asetus | Kuvaus | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hyväksyn. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | Kuinka usein sijainti julkaistaan. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | Katso [MQTT](mqtt) saadaksesi yksityiskohtaisen käyttöoppaan, joka sisältää salauksen, tietosuojan ja välityspalvelimen määrityksen. -### Sarjaporttimoduuli +### Serial module Mahdollistaa sarjaporttiviestinnän ulkoisten laiteintegraatioiden kanssa (GPS-moduulit, anturit tai mukautettu laitteisto). Kun tämä on käytössä, radion sarjaportti voi lähettää ja vastaanottaa protobuf- tai tekstimuotoista dataa, jolloin ulkoiset mikrokontrollerit tai tietokoneet voivat olla vuorovaikutuksessa verkon kanssa. @@ -76,9 +75,9 @@ Mahdollistaa sarjaporttiviestinnän ulkoisten laiteintegraatioiden kanssa (GPS-m | Aikakatkaisu | Kuinka kauan odotetaan ennen kuin saapuva viesti katsotaan kokonaiseksi | | Korvaa konsolin sarjaportti | Ota käyttöön portti, jota virheenkorjauskonsoli normaalisti käyttää | -### Ulkoisten ilmoitusten moduuli +### External Notification module -Ohjaa radion laitteiston summeri-, LED- tai värinähälytyksiä. Hyödyllinen laitteille, joiden täytyy ilmoittaa fyysisesti viestin saapumisesta — erityisen hyödyllinen valvomattomissa tai ulkokäyttöön asennetuissa laitteissa. +Controls buzzer, LED, or vibration alerts on your node hardware. Hyödyllinen laitteille, joiden täytyy ilmoittaa fyysisesti viestin saapumisesta — erityisen hyödyllinen valvomattomissa tai ulkokäyttöön asennetuissa laitteissa. Käynnistimiä on kaksi – saapuva **viesti** ja vastaanotettu **BEL**-ohjausmerkki – ja kumpikin voi ohjata LED-valoa, summeria ja värinämoottoria erikseen, joten käytettävissä on kuusi kytkintä. @@ -93,11 +92,11 @@ Käynnistimiä on kaksi – saapuva **viesti** ja vastaanotettu **BEL**-ohjausme | Ulostulon värinä (GPIO) | Värinämoottori on kytketty nastaan | | Käytä PWM-äänimerkkiä | Ohjaa summeria PWM:llä, jolloin voidaan toistaa ääniä yhden kiinteän taajuuden sijaan | | Käytä I2S protokollaa äänimerkille | Lähetä hälytys sen sijaan I2S-äänilähdön kautta | -| Ulostulon kesto (millisekuntia) | Kuinka kauan yksittäinen hälytys kestää | -| Hälytysaikakatkaisu (sekuntia) | Toista hälytystä tämän ajan, kunnes se kuitataan. 0 poistaa toistuvan muistutuksen käytöstä | +| GPIO Output Duration | Kuinka kauan yksittäinen hälytys kestää | +| Toistokehotuksen aikakatkaisu | Toista hälytystä tämän ajan, kunnes se kuitataan. 0 poistaa toistuvan muistutuksen käytöstä | | Soittoääni | PWM-summerilla toistettava RTTTL-soittoääni. Voidaan tuoda tiedostosta | -### Varastoi & välitä -moduuli +### Store & Forward module Puskuroi viestejä radioille, jotka ovat tilapäisesti poissa verkosta, ja toimittaa ne, kun nämä radiot yhdistyvät uudelleen. Välttämätön verkoissa, joissa radiot siirtyvät säännöllisesti kuuluvuusalueelle ja sen ulkopuolelle — varmistaa, etteivät viestit katoa lyhyiden yhteyskatkosten aikana. @@ -112,7 +111,7 @@ Puskuroi viestejä radioille, jotka ovat tilapäisesti poissa verkosta, ja toimi > 💡 **Vinkki:** Varastoi & välitä toimii parhaiten radioissa, joissa on runsaasti muistia (ESP32 ja PSRAM). Router-roolin radiot ovat ihanteellisia ehdokkaita, koska ne ovat yleensä jatkuvasti käynnissä. -### Kuuluvuustesti-moduuli +### Range Test module > ⚠️ **Varoitus:** Kuuluvuustesti toimii vain suojatulla ensisijaisella kanavalla. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -121,37 +120,37 @@ Puskuroi viestejä radioille, jotka ovat tilapäisesti poissa verkosta, ja toimi Automaattinen kuuluvuustestityökalu radioiden välisen yhteyden laadun arviointiin. Kun toiminto on käytössä, radio lähettää säännöllisesti testiviestejä kasvavilla laskuriarvoilla. Vastaanottava radio kirjaa nämä viestit, jolloin voit myöhemmin kävellä tai ajaa pois ja analysoida, millä etäisyydellä viestien saapuminen loppui. -| Asetus | Kuvaus | -| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Kuuluvuustesti käytössä | Ota kuuluvuustesti käyttöön | -| Viestien lähetyksen aikaväli (sekuntia) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Tallenna .CSV (ESP32 ainoastaan) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Asetus | Kuvaus | +| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Kuuluvuustesti käytössä | Ota kuuluvuustesti käyttöön | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Tallenna .CSV (ESP32 ainoastaan) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetriamoduuli +### Telemetry module Määrittää, mitä telemetriatietoja radiosi jakaa verkkoon. Telemetria sisältää laitteen kuntoon liittyviä tietoja (akun varaustaso, käyttöaika) sekä ympäristöanturien tietoja (lämpötila, kosteus, ilmanpaine). Jokaisella neljällä mittausryhmällä on oma käyttöönottokytkin ja oma mittausväli, joten esimerkiksi akun tila voidaan raportoida usein ja anturitiedot harvemmin. -| Asetus | Kuvaus | -| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Lähetä laitteen telemetriatiedot | Laitemittausten pääkytkin. Näkyy vain laiteohjelmistoversiossa 2.7.12 ja uudemmissa | -| Laitemittareiden päivitysväli | How often to report battery, uptime and channel utilization | -| Ympäristötietojen moduuli käytössä | Raportoi liitettyjen ympäristöanturien tiedot | -| Ympäristömittareiden päivitysväli | Kuinka usein tiedot raportoidaan | -| Näytä ympäristötiedot näytöllä | Näytä nämä tiedot myös radion omalla näytöllä | -| Käytä Fahrenheit yksikköä | Käytä radion näytössä Fahrenheit-asteita. Tämä koskee vain radion näyttöä – sovellus käyttää puhelimesi alueasetuksia, katso [Yksiköt ja alue](units-and-locale) | -| Ilmanlaadun tietojen moduuli käytössä | Raportoi hiukkas- ja CO₂-anturin tiedot | -| Ilmanlaatumittareiden päivitysväli | Kuinka usein tiedot raportoidaan | -| Virrankulutuksen moduuli käytössä | Raportoi kanavakohtaiset jännite- ja virtamittaukset | -| Virtamittareiden päivitysväli | Kuinka usein tiedot raportoidaan | -| Virrankulutuksen näyttö käytössä | Näytä virtamittaukset myös radion omalla näytöllä | +| Asetus | Kuvaus | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Lähetä laitteen telemetriatiedot | Laitemittausten pääkytkin. Näkyy vain laiteohjelmistoversiossa 2.7.12 ja uudemmissa | +| Laitemittareiden päivitysväli | How often to report battery, uptime and channel utilization | +| Ympäristötietojen moduuli käytössä | Raportoi liitettyjen ympäristöanturien tiedot | +| Ympäristömittareiden päivitysväli | Kuinka usein tiedot raportoidaan | +| Näytä ympäristötiedot näytöllä | Näytä nämä tiedot myös radion omalla näytöllä | +| Käytä Fahrenheit yksikköä | Käytä radion näytössä Fahrenheit-asteita. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Ilmanlaadun tietojen moduuli käytössä | Raportoi hiukkas- ja CO₂-anturin tiedot | +| Ilmanlaatumittareiden päivitysväli | Kuinka usein tiedot raportoidaan | +| Virrankulutuksen moduuli käytössä | Raportoi kanavakohtaiset jännite- ja virtamittaukset | +| Virtamittareiden päivitysväli | Kuinka usein tiedot raportoidaan | +| Virrankulutuksen näyttö käytössä | Näytä virtamittaukset myös radion omalla näytöllä | Katso [Telemetria ja anturit](telemetry-and-sensors) saadaksesi tietoa tuetuista antureista ja määrityssuosituksista. -### Valmiiden viestien moduuli +### Canned Message module -Valmiiksi määritetyt viestit, joita voidaan lähettää radion fyysisillä painikkeilla (radioille, joissa on kiertokoodain, näppäimistö tai vastaava syöttölaite). Määritä luettelo pikaviesteistä, jotka voidaan lähettää ilman yhdistettyä puhelinta — ihanteellinen kenttäkäyttöön. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Määritä luettelo pikaviesteistä, jotka voidaan lähettää ilman yhdistettyä puhelinta — ihanteellinen kenttäkäyttöön. | Asetus | Kuvaus | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | @@ -164,7 +163,7 @@ Valmiiksi määritetyt viestit, joita voidaan lähettää radion fyysisillä pai | Ylös/Alas/Valitse syöte käytössä | Erillinen, yksinkertaisempi syöttötapa, jossa käytetään ylös-/alas-/valitse-painikkeita kiertokoodaimen sijaan | | ~~Salli syöttölähde~~ | ⚠️ **Vanhentunut** protobuf-rakenteessa | -### Äänimoduuli +### Audio module Codec2-äänituki matalan kaistanleveyden puheviestintään verkossa. Tämä on **kokeellinen** ominaisuus, joka koodaa puheen erittäin pieniksi datapaketeiksi käyttäen Codec2-koodekkia. @@ -180,11 +179,11 @@ Codec2-äänituki matalan kaistanleveyden puheviestintään verkossa. Tämä on > ℹ️ **Huomautus:** Ääniominaisuus edellyttää yhteensopivaa laitteistoa (I2S-mikrofoni ja -kaiutin). Äänenlaatu on hyvin matalakaistainen — ajattele "ymmärrettävää radiopuhetta", ei puhelinlaatua. -### Etälaitteiston moduuli +### Remote Hardware module GPIO-ohjaus mesh-verkon kautta. Mahdollistaa etäradion lukea tai kirjoittaa GPIO-nastoja toisessa radiossa — hyödyllinen releiden aktivointiin, kytkimien lukemiseen tai ulkoisen laitteiston ohjaamiseen etäältä. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Asetus | Kuvaus | | ------------------------------------ | --------------------------------------------------------------------------- | @@ -192,19 +191,19 @@ GPIO-ohjaus mesh-verkon kautta. Mahdollistaa etäradion lukea tai kirjoittaa GPI | Salli määrittämättömän pinnin käyttö | Salli pääsy mihin tahansa GPIO-nastaan (tietoturvariski) | | Käytettävissä olevat pinnit | Enintään 4 tämän radion etälukuun tai kirjoitukseen tarjoamaa GPIO-pinniä | -### Naapuritieto-moduuli +### Neighbor Info module Lähettää tietoa suoraan kuulluista naapureista mahdollistaen verkon topologian kartoituksen. Jokainen käyttöön otettu radio jakaa säännöllisesti luettelon muista radioista, jotka se kuulee, sekä niiden signaalin laadun. -| Asetus | Kuvaus | -| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Naapuritiedot käytössä | Ota naapuritiedon lähetys käyttöön | -| Päivityksen aikaväli (sekuntia) | Kuinka usein naapuriluettelo lähetetään | -| Lähetä LoRa:n kautta | Lähetä myös naapuritiedot LoRa:n kautta, ei pelkästään MQTT:n tai puhelimen kautta. Ei käytettävissä kanavalla, joka käyttää oletusavainta ja nimeä | +| Asetus | Kuvaus | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Naapuritiedot käytössä | Ota naapuritiedon lähetys käyttöön | +| GPS-kyselyn aikaväli | Kuinka usein naapuriluettelo lähetetään | +| Lähetä LoRa:n kautta | Lähetä myös naapuritiedot LoRa:n kautta, ei pelkästään MQTT:n tai puhelimen kautta. Ei käytettävissä kanavalla, joka käyttää oletusavainta ja nimeä | Katso [Paikallinen mesh-haku](discovery), miten naapuritietoja käytetään mesh-verkon rakenteen tutkimiseen. -### Ympäristövalaistusmoduuli +### Ambient Lighting module Ohjaa tuetuissa laitteissa olevaa NeoPixeliä tai muita osoitteellisia RGB-LEDejä. Voidaan käyttää visuaalisina tilailmaisimina, ilmoitusvaloina tai koriste-efekteinä. @@ -214,48 +213,48 @@ Ohjaa tuetuissa laitteissa olevaa NeoPixeliä tai muita osoitteellisia RGB-LEDej | Virta | LED-virran rajoitus (0–31) | | Punainen / vihreä / sininen | Yksittäisten värikanavien arvot (0–255) | -### Tunnistusanturimoduuli +### Detection Sensor module Muuttaa radiosi liike- tai ovitunnistimeen perustuvaksi hälytysjärjestelmäksi. Kun GPIO-pinni havaitsee tilamuutoksen (liike havaittu, ovi avattu), radio lähettää hälytysviestin verkkoon. -| Asetus | Kuvaus | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -| Tunnistinsensori käytössä | Ota tunnistusanturi käyttöön | -| GPIO-pinni valvontaa varten | Anturiin kytketty GPIO-pinni | -| Tunnistuksen tyyppi | Miten pinnin tila vastaa havaitsemistapahtumaa (esim. aktiivinen korkea/matala taso tai reunalaukaisu) | -| Käytä INPUT_PULLUP tilaa | Ota pinnin sisäinen ylösvetovastus käyttöön | -| Minimilähetys (sekuntia) | Hälytyslähetysten vähimmäisaika | -| Tilatiedon lähetys (sekuntia) | Tilatiedon lähetysväli | -| Lähetä äänimerkki hälytyssanoman kanssa | Sisällytä soittomerkkimerkki hälytyksiin | -| Käyttäjäystävällinen nimi | Tälle anturille määritetty nimi | +| Asetus | Kuvaus | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Tunnistinsensori käytössä | Ota tunnistusanturi käyttöön | +| GPIO-pinni valvontaa varten | Anturiin kytketty GPIO-pinni | +| Tunnistuksen tyyppi | Miten pinnin tila vastaa havaitsemistapahtumaa (esim. aktiivinen korkea/matala taso tai reunalaukaisu) | +| Käytä INPUT_PULLUP tilaa | Ota pinnin sisäinen ylösvetovastus käyttöön | +| Minimum time between detection broadcasts | Hälytyslähetysten vähimmäisaika | +| State Broadcast Interval | Tilatiedon lähetysväli | +| Lähetä äänimerkki hälytyssanoman kanssa | Sisällytä soittomerkkimerkki hälytyksiin | +| Käyttäjäystävällinen nimi | Tälle anturille määritetty nimi | -### PAX-laskurimoduuli +### Paxcounter module Henkilöt, jotka lasketaan WiFi- ja BLE-hakukyselyiden perusteella. Laskee lähellä olevia laitteita kuuntelemalla passiivisesti koepyyntöjä, joita puhelimet ja kannettavat tietokoneet lähettävät etsiessään verkkoja. Saatavilla vain ESP32-laitteissa. -| Asetus | Kuvaus | -| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| PAX-laskuri käytössä | Ota henkilölaskenta käyttöön | -| Päivityksen aikaväli (sekuntia) | Kuinka usein laskentatiedot raportoidaan | -| WiFi-signaalin RSSI-kynnys | Ohita tätä heikommat WiFi-hakukyselyt, jotta kauempana olevia laitteita ei lasketa mukaan (oletus: –80 dBm) | -| BLE-signaalin RSSI-kynnys | Sama raja-arvo BLE-mainospaketeille (oletus: -80 dBm) | +| Asetus | Kuvaus | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| PAX-laskuri käytössä | Ota henkilölaskenta käyttöön | +| GPS-kyselyn aikaväli | Kuinka usein laskentatiedot raportoidaan | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | Sama raja-arvo BLE-mainospaketeille (oletus: -80 dBm) | > 💡 **Vinkki:** PAX-laskuri on hyödyllinen jalankulkijamäärien arviointiin retkeilyreittien lähtöpisteissä, tapahtumapaikoilla tai muissa kohteissa. Laskentatulokset ovat arvioita — yhdellä henkilöllä voi olla useita laitteita mukana. -### Tilaviestimoduuli +### Status Message module Tilaviestillä ei ole omaa moduulinäkymää. Sitä muokataan yhdessä muiden radion tunnistetietojen kanssa kohdassa [Asetukset → Radio ja käyttäjä](settings-radio-user#user-profile). -### Mesh Beacon -moduuli +### Mesh Beacon module Lähettää kutsun mesh-verkkoosi ja vastaanottaa muiden lähettämiä kutsuja. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK-moduuli +### TAK module Team Awareness Kit -integraatio yhteensopivuutta varten ATAK- ja WinTAK-järjestelmien kanssa. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. Katso [TAK-integraatio](tak) saadaksesi tarkemmat määritys- ja käyttöohjeet. @@ -276,32 +275,33 @@ Määritä etänä radiot, jotka jakavat saman ylläpitoavaimen: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Toiminto | Mitä se tekee | -| -------------------------- | ------------------------------------------------------------------------------------------------------ | -| Aseta aika | Sends your phone's clock to the radio | -| Käynnistä uudelleen | Restarts the radio | -| Sammuta | Powers the radio down | -| Palauta tehdasasetukset | Returns every setting to its factory default | -| Tyhjennä NodeDB-tietokanta | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Toiminto | Mitä se tekee | +| -------------------------- | ---------------------------------------------------------------------------------------------- | +| Aseta aika | Sends your phone's clock to the node | +| Käynnistä uudelleen | Restarts the node | +| Sammuta | Powers the node down | +| Palauta tehdasasetukset | Returns every setting to its factory default | +| Tyhjennä NodeDB-tietokanta | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Varmuuskopiointi ja palautus -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Lisäasetukset **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Tyhjennä NodeDB-tietokanta -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -311,7 +311,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -321,12 +321,39 @@ Avaa **Paketit**- ja **Sovelluslokit**-välilehdet diagnostiikkatietojen tarkast ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Tietoja @@ -346,7 +373,7 @@ may redistribute under the same license. **Tietoja**-kohdasta avattava näkymä, jossa luetellaan kaikki sovelluksen käyttämät avoimen lähdekoodin kirjastot lisensseineen. Luettelo luodaan käännöshetkellä AboutLibraries-kirjastolla. Tätä kutsuttiin aiemmin lisenssinäkymäksi. -### Etähallinnan vianmääritys +### Troubleshooting remote admin - **Ei vastausta kohderadiolta** — kohderadio voi olla kuuluvuusalueen ulkopuolella, poissa verkosta tai siinä voi olla eri ylläpitoavain. Varmista, että ylläpitoavain on sama molemmissa radioissa. - **Muutokset eivät tule voimaan** — jotkin asetukset edellyttävät uudelleenkäynnistystä ennen kuin ne astuvat voimaan. Kokeile Uudelleenkäynnistystä tallennuksen jälkeen. @@ -354,6 +381,6 @@ may redistribute under the same license. ## Aiheeseen liittyvät aiheet -- [Asetukset — Radio ja käyttäjä](settings-radio-user) — radion ja käyttäjäprofiilin keskeiset asetukset +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Moduulien määritysviite](https://meshtastic.org/docs/configuration/module) — yksityiskohtainen moduulidokumentaatio meshtastic.org-sivustolla - [UKK](https://meshtastic.org/docs/faq/) – usein kysytyt kysymykset meshtastic.org -sivustolla diff --git a/docs/fi-rFI/user/settings-radio-user.md b/docs/fi-rFI/user/settings-radio-user.md index 8eaf4fc852..b52e8c2c4f 100644 --- a/docs/fi-rFI/user/settings-radio-user.md +++ b/docs/fi-rFI/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Asetukset — Radio ja käyttäjä -parent: Käyttöopas nav_order: 7 -last_updated: 2026-09-11 -description: Määritä radion laitteisto, LoRa-esiasetukset, käyttäjäprofiili, sijainnin jakaminen, virranhallinta ja tietoturva. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - asetukset - radion asetukset @@ -13,14 +12,14 @@ aliases: # Asetukset — Radio ja käyttäjä -Määritä radion käyttäjätiedot, alue ja LoRa-asetukset, sijainti- ja virta-asetukset, verkko- ja Bluetooth-yhteydet sekä suojausasetukset. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Asetukset käyttävät tavallisia asetussäätimiä — pudotusvalikoita, kytkimiä ja liukusäätimiä: @@ -36,19 +35,19 @@ Asetukset käyttävät tavallisia asetussäätimiä — pudotusvalikoita, kytkim On **Settings → User**. -| Asetus | Kuvaus | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Pitkä nimi | Näyttönimesi (enintään 39 merkkiä) | -| Lyhytnimi | 4-merkkinen lyhytnimi | -| Tilaviesti | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Edellyttää laiteohjelmistoversiota 2.8 tai uudempaa. Muussa tapauksessa tätä ei näytetä | -| Ei vastaanota viestejä | Merkitsee radion sellaiseksi, jolle kenenkään ei pitäisi yrittää lähettää viestejä – tarkoitettu valvomattomalle tai infrastruktuuriradiolle. Muut sovellukset piilottavat sen yhteystietoluettelosta. Edellyttää yhteensopivaa laiteohjelmistoa | -| Lisensoitu radioamatööri (HAM) | Ota käyttöön, jos sinulla on radioamatöörilupa (sallii suuremman lähetystehon). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Asetus | Kuvaus | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pitkä nimi | Näyttönimesi (enintään 39 merkkiä) | +| Lyhytnimi | 4-merkkinen lyhytnimi | +| Tilaviesti | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Edellyttää laiteohjelmistoversiota 2.8 tai uudempaa. Muussa tapauksessa tätä ei näytetä | +| Ei vastaanota viestejä | Merkitsee radion sellaiseksi, jolle kenenkään ei pitäisi yrittää lähettää viestejä – tarkoitettu valvomattomalle tai infrastruktuuriradiolle. Muut sovellukset piilottavat sen yhteystietoluettelosta. Edellyttää yhteensopivaa laiteohjelmistoa | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Muutosten käyttöönotto +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. -Tilaviesti tallennetaan samalla **Tallenna**-painikkeella, mutta se ei koskaan käynnistä radiota uudelleen. Kuten muitakin tämän näkymän asetuksia, sitä voidaan muokata etähallittavassa radiossa. For your own radio there is a +Tilaviesti tallennetaan samalla **Tallenna**-painikkeella, mutta se ei koskaan käynnistä radiota uudelleen. Kuten muitakin tämän näkymän asetuksia, sitä voidaan muokata etähallittavassa radiossa. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -65,7 +64,7 @@ On **Settings → Device configuration → Device**. | Uudelleenlähetyksen tila | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Laitteen tietojen lähetyksen aikaväli | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Kaksoisnapautus painikkeena | Käsittele kaksoisnapautus painikkeen painalluksena | Ei käytössä | -| Kolmoisklikkaus Ad Hoc -pingille | Lähetä kertaluonteinen sijaintipyyntö kolminkertaisella painalluksella | Ei käytössä | +| Kolmoisklikkaus Ad Hoc -pingille | Lähetä kertaluonteinen sijaintipyyntö kolminkertaisella painalluksella | Käytössä | | Ledin valvontasignaali | Vilkuta tilan merkkilediä säännöllisesti | Käytössä | | Aikavyöhyke | Laitteen kellon POSIX-aikavyöhyke. Painikkeilla voit kopioida puhelimesi aikavyöhykkeen tai tyhjentää kentän | — | | Painikkeen / summerin GPIO | Lisäasetukset: GPIO-nastat, joihin painike ja summeri on kytketty | — | @@ -74,32 +73,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Asetus | Kuvaus | Oletus | -| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| Alue | Taajuusalueen sääntelyalue. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Ei asetettu (on määritettävä) | -| Esiasetukset | Nopeuden ja kantaman välinen kompromissi | LongFast | -| Hyppyjen määrä | Suurin hyppyjen määrä | 3 | -| Lähetysteho | Lähetysteho (dBm): 0 = alueen sallima enimmäisteho | 0 (alueen enimmäisteho) | -| Taajuuden ohitus | Ohittaa lasketun käyttötaajuuden (MHz). Ei siirrä laskettua arvoa – jätä arvoksi 0, ellet tarvitse tiettyä taajuutta | 0 (käytä laskettua arvoa) | -| Käytä esiasetusta | Oletusarvoisesti käytössä. Poista tämä käytöstä, jos haluat määrittää hajotuskerroin-, koodausnopeus- ja kaistanleveysasetukset käsin modeemiesiasetuksen sijaan | Käytössä | -| Levennyskerroin (Spread Factor) | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | Esiasetuksesta | -| Koodausnopeus | Vain manuaalitilassa: 5–8. Suurempi virheenkorjaus lisää lähetysaikaa | Esiasetuksesta | -| Kaistanleveys | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | Esiasetuksesta | -| Taajuuspaikka | Määrittää, mitä alueen taajuusväliä käytetään. 0 muodostetaan ensisijaisen kanavan nimestä | 0 (automaattinen) | -| Lähetys käytössä | Tämän poistaminen käytöstä tekee radiosta vain vastaanottavan | Käytössä | -| Ohita käyttöaste (Duty Cycle) | Ohittaa alueen lähetysajan käyttörajoituksen. Laitonta useimmilla alueilla. Ota käyttöön vain, jos radioamatöörilupasi sallii sen | Pois | -| Ohita MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| MQTT päällä | Salli yhdyskäytävien välittää pakettisi MQTT:hen | Pois | -| RX tehostettu vahvistus | Lisävastaanottovahvistus SX126x-radioille. Kuluttaa hieman enemmän virtaa | Pois | -| PA tuuletin pois käytöstä | Poista päätevahvistimen tuuletin käytöstä laitteissa, joissa sellainen on | Pois | +| Asetus | Kuvaus | Oletus | +| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| Alue | Taajuusalueen sääntelyalue. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Ei asetettu (on määritettävä) | +| Esiasetukset | Nopeuden ja kantaman välinen kompromissi | LongFast | +| Hyppyjen määrä | Suurin hyppyjen määrä | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (alueen enimmäisteho) | +| Taajuuden ohitus | Ohittaa lasketun käyttötaajuuden (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (käytä laskettua arvoa) | +| Käytä esiasetusta | Oletusarvoisesti käytössä. Poista tämä käytöstä, jos haluat määrittää hajotuskerroin-, koodausnopeus- ja kaistanleveysasetukset käsin modeemiesiasetuksen sijaan | Käytössä | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Levennyskerroin (Spread Factor) | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | Esiasetuksesta | +| Koodausnopeus | Vain manuaalitilassa: 5–8. Suurempi virheenkorjaus lisää lähetysaikaa | Esiasetuksesta | +| Kaistanleveys | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | Esiasetuksesta | +| Taajuuspaikka | Määrittää, mitä alueen taajuusväliä käytetään. 0 muodostetaan ensisijaisen kanavan nimestä | 0 (automaattinen) | +| Lähetys käytössä | Tämän poistaminen käytöstä tekee radiosta vain vastaanottavan | Käytössä | +| Ohita käyttöaste (Duty Cycle) | Ohittaa alueen lähetysajan käyttörajoituksen. Laitonta useimmilla alueilla. Ota käyttöön vain, jos radioamatöörilupasi sallii sen | Pois | +| Ohita MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| MQTT päällä | Salli yhdyskäytävien välittää pakettisi MQTT:hen | Pois | +| RX tehostettu vahvistus | Lisävastaanottovahvistus SX126x-radioille. Kuluttaa hieman enemmän virtaa | Pois | +| PA tuuletin pois käytöstä | Poista päätevahvistimen tuuletin käytöstä laitteissa, joissa sellainen on | Pois | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Tärkeää:** Käyttö väärällä alueasetuksella voi rikkoa paikallisia radiomääräyksiä. Katso [alueasetusten määritysopas](https://meshtastic.org/docs/getting-started/initial-config) meshtastic.org-sivustolta saadaksesi lisätietoja. -### Esiasetukset +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Vinkki:** **SNR-raja**-arvot ovat tarkoituksella negatiivisia. LoRa pystyy purkamaan signaaleja _kohinatason alapuolelta_, joten negatiivisempi raja tarkoittaa, että esiasetus sietää heikomman ja kohinaisemman signaalin (suurempi kantama). Katso [Miten signaalimittari toimii](signal-meter) saadaksesi täydellisen selityksen. @@ -119,13 +125,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz -alue (62,5 kHz BW); verrattavissa Long Fast -asetukseen | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Vanhentunut** — edelleen valittavissa, mutta voidaan poistaa tulevassa laiteohjelmistoversiossa | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Vanhentunut** — edelleen valittavissa, mutta voidaan poistaa tulevassa laiteohjelmistoversiossa | > ℹ️ **Huomautus:** Tässä taulukossa käytetään yleisesti käytössä olevia lyhyitä nimiä. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Modeemiesiasetuksen valitseminen +#### Choosing a modem preset Modeemiesiasetus määrittää tärkeimmän kompromissin **kantaman** ja **tiedonsiirtonopeuden** välillä: @@ -140,38 +146,36 @@ Modeemiesiasetus määrittää tärkeimmän kompromissin **kantaman** ja **tiedo - **Kiinteät infrastruktuurilinkit:** Käytä **Short Turbo**- tai **Long Turbo** -asetusta erillisille pisteestä pisteeseen -linkeille, joissa on hyvät antennit ja suora näköyhteys. - **Sekaverkot:** Pysy **Long Fast** -asetuksessa — se on yhteisön oletusasetus ja varmistaa yhteensopivuuden alueesi muiden käyttäjien kanssa. -Kaikkien samalla kanavalla olevien radioiden on käytettävä samaa modeemiesiasetusta. Radiot, joiden modeemiesiasetukset eivät täsmää, eivät voi viestiä keskenään, vaikka ne käyttäisivät samaa taajuutta ja salausavainta. +Kaikkien samalla kanavalla olevien radioiden on käytettävä samaa modeemiesiasetusta. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. [Modeemiesiasetukset](#modem-presets)-taulukon kantama-arviot perustuvat tasaiseen maastoon ja vaatimattomiin antenneihin. Korkeuseroetu (mäki, rakennuksen katto) kasvattaa käytännön kantamaa huomattavasti. Hyvin sijoitettu Long Fast -asetusta käyttävä Router voi usein toimia paremmin kuin maan tasalla oleva Long Slow -asetusta käyttävä radio. ### Näytön asetukset -On **Settings → Device configuration → Display**. Nämä ohjaavat **radion omaa näyttöä**, eivät sovellusta. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. | Asetus | Kuvaus | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Näytön päälläoloaika | Kuinka kauan näyttö pysyy päällä ennen siirtymistä lepotilaan | -| Karusellin aikaväli | Kuinka usein radio vaihtaa näyttönäkymää automaattisesti | +| Karusellin aikaväli | How often the node cycles between screens on its own | | Näyttötila | Laiteohjelmiston käyttämä näytön asettelu/tiheys | -| Näyttöyksiköt | Metrinen tai imperiaalinen radion näytössä | -| Käytä 12 tunnin kelloa | Näytä radion kello 12 tunnin muodossa 24 tunnin sijaan | +| Näyttöyksiköt | Metric or Imperial on the node's screen | +| Käytä 12 tunnin kelloa | Show the node's clock as 12-hour rather than 24-hour | | Lihavoitu otsikko | Näytä näytön otsikkoteksti lihavoituna | | Käännä näyttö | Kierrä näyttö 180° ylösalaisin asennusta varten | -| OLED-tyyppi | Auto, SSD1306, SH1106, SH1107 | -| Herätä napautuksesta tai liikkeestä | Laita näyttö päälle, kun radioon kosketaan tai sitä liikutetaan | -| Kompassin suuntaus | Kompassinäytön kiertopoikkeama (0°, 90°, 180°, 270°) | +| OLED-tyyppi | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Herätä napautuksesta tai liikkeestä | Light the screen when the node is tapped or moved | +| Kompassin suuntaus | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | | Osoita aina pohjoiseen | Lukitsee kompassinäytön näyttämään pohjoiseen sen sijaan, että se kääntyisi kulkusuunnan mukaan. Riippumaton kompassin suunnasta — kumpikaan ei korvaa toista | ### Sijainnin asetukset On **Settings → Device configuration → Position**. -> ⚠️ **Tärkeää:** Tämän näkymän tallentaminen käynnistää radion aina uudelleen. - | Asetus | Kuvaus | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | GPS-tila (fyysinen laitteisto) | Kolmitilainen: GPS käytössä, poistettu käytöstä tai ei käytettävissä. Ei pelkkä käytössä / poistettu käytöstä -asetus | -| GPS-kyselyn aikaväli | Kuinka usein radio pyytää GPS:ltä sijainnin | +| GPS-kyselyn aikaväli | How often the node asks its GPS for a fix | | Lähetyksen aikaväli | Kuinka usein sijainti jaetaan mesh-verkkoon | | Älykäs sijainti | Lähettää sijainnin liikkeen perusteella pelkän aikavälin sijaan | | Älykäs aikaväli | Älykäs sijainti -toiminnon ollessa käytössä lyhin sallittu aika sijaintilähetysten välillä | @@ -186,10 +190,10 @@ On **Settings → Device configuration → Power**. | Asetus | Kuvaus | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| Ota virransäästötila käyttöön | Anna radion siirtyä mahdollisimman herkästi lepotilaan toimintojen välillä | +| Ota virransäästötila käyttöön | Let the node sleep aggressively between activity | | Sammuta virran katketessa | Sammuta laite, kun ulkoinen virta katkeaa | | Super-syväunen kesto | Kuinka kauan syvin lepotila kestää | -| Vähimmäisherätyksen kesto | Kuinka vähän aikaa radio pysyy hereillä heräämisen jälkeen | +| Vähimmäisherätyksen kesto | The shortest time the node stays awake once woken | | Bluetoothin odotusaika | Kuinka kauan odotetaan puhelimen yhteyden muodostumista ennen lepotilaan siirtymistä | | ADC-kertoimen ohitus | Ota akun jännitemittauksille käyttöön manuaalinen korjaus | | Korvaava AD-muuntimen kerroin | Korjauskerroin, käytössä vain kun manuaalinen korjaus on käytössä | @@ -197,13 +201,13 @@ On **Settings → Device configuration → Power**. ### Verkon asetukset -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Asetus | Kuvaus | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| WiFi käytössä | Ota WiFi-radio käyttöön (ESP32-radiot) | +| WiFi käytössä | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Verkon nimi, johon yhdistetään. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Salasana | Verkon salasana | | Ethernet käytössä | Käytä langallista yhteyttä sitä tukevalla laitteistolla | @@ -217,9 +221,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth asetukset -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Tärkeää:** Tämän näkymän tallentaminen käynnistää radion aina uudelleen. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Asetus | Kuvaus | | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | @@ -231,18 +233,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Tärkeää:** Tämän näkymän tallentaminen käynnistää radion aina uudelleen. - | Asetus | Kuvaus | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Julkinen avain | Radiosi julkinen avain (vain luku) | | Ylläpitäjän avain | Avaimet, joilla tätä radiota voidaan hallita etänä – enintään kolme | -| Yksityinen avain | Radiosi yksityinen avain (säilytä turvallisesti). Näytetään peitettynä, kun tarkastelet toista radiota etähallinnan kautta — laiteohjelmisto ei lähetä sitä | +| Yksityinen avain | Radiosi yksityinen avain (säilytä turvallisesti). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Luo uusi yksityinen avain | Luo tälle radiolle uuden avainparin vahvistuksen jälkeen. Kaikkien vanhan avaimesi tunteneiden vertaisradioiden on opittava uusi avain | | ~~Ylläpitokanava käytössä~~ | ⚠️ Poistettu — määritetään nyt automaattisesti, kun ylläpitoavain asetetaan | | Sarjaporttikonsoli | Serial console over the Stream API | -| Vianetsintälokirajapinta käytössä | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Hallintatila | Rajoita muiden kuin järjestelmänvalvojan tekemiä kanavamuutoksia. Voidaan valita vasta, kun järjestelmänvalvojan avain on määritetty | +| Vianetsintälokirajapinta käytössä | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Hallintatila | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Voidaan valita vasta, kun järjestelmänvalvojan avain on määritetty | | Varmuuskopioi avaimet | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Palauta avaimet | Kirjoita varmuuskopioidut avaimet takaisin radioon (käytettävissä, kun varmuuskopio on olemassa) | | Poista avaimen varmuuskopio | Remove the stored key backup from this phone | @@ -250,9 +250,13 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lukitustila -Lukitustila salaa laitteen tallennustilan ja edellyttää tunnuslausetta jokaiselle yhteydelle. Edellyttää sitä tukevaa laiteohjelmistoa. Muussa tapauksessa tätä ei näytetä. +Lukitustila salaa laitteen tallennustilan ja edellyttää tunnuslausetta jokaiselle yhteydelle. It needs +supporting firmware; the row doesn't appear otherwise. -Käyttöönoton yhteydessä sinua pyydetään määrittämään ja vahvistamaan tunnuslause sekä vahvistamaan, että **se lukitsee virheenkorjausportin (SWD)** sitä tukevassa laitteistossa. Voit poistaa lukitustilan käytöstä milloin tahansa tunnuslauseella, ja laitteen täydellinen tyhjennys palauttaa laitteiston joka tapauksessa. +Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Tunnuslauseen lisäksi määrität rajat, joiden täyttyessä istunto päättyy automaattisesti: diff --git a/docs/fi-rFI/user/signal-meter.md b/docs/fi-rFI/user/signal-meter.md index 1498d4d4f0..5ed409b680 100644 --- a/docs/fi-rFI/user/signal-meter.md +++ b/docs/fi-rFI/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: Kuinka Meshtastic-signaalimittari toimii -parent: Käyttöopas nav_order: 15 last_updated: 2026-09-09 description: Miten signaalimittari arvioi signaalin laadun SNR-arvon perusteella suhteessa LoRa-modeemiesiasetukseen — hajaspektri, esiasetukset ja mitä palkit todellisuudessa tarkoittavat. diff --git a/docs/fi-rFI/user/tak.md b/docs/fi-rFI/user/tak.md index 77af46783e..2a3d488613 100644 --- a/docs/fi-rFI/user/tak.md +++ b/docs/fi-rFI/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK-integraatio -parent: Käyttöopas nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: ATAK:n ja WinTAK:n yhteentoimivuus — CoT-sijaintijako, TAK-roolit ja lisäosien käyttöönotto. aliases: - tak @@ -98,6 +97,7 @@ Kun asetukset on määritetty: - Viestit voivat välittyä mesh-verkon ja TAK-verkon välillä - Sijaintipäivitykset kulkevat kaksisuuntaisesti Meshtasticin ja TAK-järjestelmän välillä - TAK Tracker -radiot lähettävät PLI-sijaintia automaattisesti — niiden sijainti näkyy ATAK-kartoilla ilman erillistä ATAK-asetusta +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Huomautus:** TAK-integraatio edellyttää tiettyjä radiorooleja. Tavalliset Client-roolin radiot eivät osallistu automaattisesti TAK-toimintoihin — mutta kun **Mesh to CoT Converter** on käytössä, ne näkyvät silti ATAK-kartalla yhteystietoina. diff --git a/docs/fi-rFI/user/telemetry-and-sensors.md b/docs/fi-rFI/user/telemetry-and-sensors.md index 55fd83d770..9bf2437aa8 100644 --- a/docs/fi-rFI/user/telemetry-and-sensors.md +++ b/docs/fi-rFI/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetria ja anturit -parent: Käyttöopas nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Anturitiedot verkossa — tuetut ympäristö-, ilmanlaatu- ja virta-anturit sekä määritys- ja katseluohjeet. aliases: - sensorit @@ -43,11 +42,12 @@ Tuetut ympäristöanturit: ### Ilmanlaatu -| Sensor | Metrijärjestelmä | Viestit | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Kaasuvastus ja IAQ | Haihtuvat orgaaniset yhdisteet | -| PMSA003I | PM1.0, PM2.5, PM10 | Katso [Ilmanlaatumittaukset](#air-quality-metrics) | -| SEN55 | PM, lämpötila, kosteus | Monianturi. NOx- ja VOC-indeksit tallennetaan ja sisällytetään CSV-vientiin, mutta niitä ei vielä näytetä kortteina tai kaavioina | +| Sensor | Metrijärjestelmä | Viestit | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Kaasuvastus ja IAQ | Haihtuvat orgaaniset yhdisteet | +| PMSA003I | PM1.0, PM2.5, PM10 | Katso [Ilmanlaatumittaukset](#air-quality-metrics) | +| SEN55 | PM, lämpötila, kosteus | Monianturi. NOx- ja VOC-indeksit tallennetaan ja sisällytetään CSV-vientiin, mutta niitä ei vielä näytetä kortteina tai kaavioina | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Maaperä @@ -58,6 +58,8 @@ Tuetut ympäristöanturit: Molemmat näkyvät tietokorteissa radion tietonäytössä muiden ympäristömittausten rinnalla. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Valo ja UV | Sensor | Metrijärjestelmä | @@ -68,17 +70,18 @@ Molemmat näkyvät tietokorteissa radion tietonäytössä muiden ympäristömitt ### Weather and Other Readings -| Metrijärjestelmä | Yksikkö | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Säteily | µR/h | Card and chart | -| Paino | kg or lb | Card only — load cells, such as a beehive scale | -| Etäisyys | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metrijärjestelmä | Yksikkö | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Säteily | µR/h | Card and chart | +| Paino | kg or lb | Card only — load cells, such as a beehive scale | +| Etäisyys | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Virranhallinnan arvot diff --git a/docs/fi-rFI/user/translate.md b/docs/fi-rFI/user/translate.md index ba05115084..56aa9118c5 100644 --- a/docs/fi-rFI/user/translate.md +++ b/docs/fi-rFI/user/translate.md @@ -1,6 +1,5 @@ --- title: Käännä sovellus -parent: Käyttöopas nav_order: 17 last_updated: 2026-09-11 description: Miten sovellus ja sen dokumentaatio käännetään Crowdinin avulla sekä ohjeet käännöksiin osallistumiseen. diff --git a/docs/fi-rFI/user/units-and-locale.md b/docs/fi-rFI/user/units-and-locale.md index 2c22a9f303..390f599f02 100644 --- a/docs/fi-rFI/user/units-and-locale.md +++ b/docs/fi-rFI/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Yksiköt, mittaus ja kieli- ja alueasetukset -parent: Käyttöopas nav_order: 16 last_updated: 2026-08-30 description: Miten sovellus muotoilee lämpötilan, etäisyyden, nopeuden ja muut mittayksiköt laitteesi alueasetusten perusteella. diff --git a/docs/fi-rFI/user/widget.md b/docs/fi-rFI/user/widget.md index 8b9aeaad51..6336592eac 100644 --- a/docs/fi-rFI/user/widget.md +++ b/docs/fi-rFI/user/widget.md @@ -1,6 +1,5 @@ --- title: Aloitusnäytön widget -parent: Käyttöopas nav_order: 20 last_updated: 2026-08-30 description: Lisää Meshtasticin aloitusnäytön widget, jotta näet yhdellä silmäyksellä yhdistetyn radiosi paikalliset tilastot ilman, että avaat sovelluksen. diff --git a/docs/fr-rCA/user/app-functions.md b/docs/fr-rCA/user/app-functions.md index 71221b6b58..2350b55875 100644 --- a/docs/fr-rCA/user/app-functions.md +++ b/docs/fr-rCA/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: Guide de l'utilisateur nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/fr-rCA/user/connections.md b/docs/fr-rCA/user/connections.md index 6112d810bf..717c0a4506 100644 --- a/docs/fr-rCA/user/connections.md +++ b/docs/fr-rCA/user/connections.md @@ -1,8 +1,7 @@ --- title: Connexions -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Réseau | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/fr-rCA/user/debug-logs.md b/docs/fr-rCA/user/debug-logs.md index 5b710a0237..57cca67b13 100644 --- a/docs/fr-rCA/user/debug-logs.md +++ b/docs/fr-rCA/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Guide de l'utilisateur nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/fr-rCA/user/desktop.md b/docs/fr-rCA/user/desktop.md index 114d3613be..83861e5803 100644 --- a/docs/fr-rCA/user/desktop.md +++ b/docs/fr-rCA/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/fr-rCA/user/discovery.md b/docs/fr-rCA/user/discovery.md index 262207e631..bed9869206 100644 --- a/docs/fr-rCA/user/discovery.md +++ b/docs/fr-rCA/user/discovery.md @@ -1,8 +1,7 @@ --- title: Découverte de maille locale -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Description | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informations sur les voisins @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/fr-rCA/user/firmware.md b/docs/fr-rCA/user/firmware.md index 3ae7ee1c68..26c8681f02 100644 --- a/docs/fr-rCA/user/firmware.md +++ b/docs/fr-rCA/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/fr-rCA/user/help-and-docs.md b/docs/fr-rCA/user/help-and-docs.md index 3e47771be6..8604e8d0a5 100644 --- a/docs/fr-rCA/user/help-and-docs.md +++ b/docs/fr-rCA/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Guide de l'utilisateur nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/fr-rCA/user/map-and-waypoints.md b/docs/fr-rCA/user/map-and-waypoints.md index 7c471e40cd..00afa292a5 100644 --- a/docs/fr-rCA/user/map-and-waypoints.md +++ b/docs/fr-rCA/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Couches cartographiques -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/fr-rCA/user/messages-and-channels.md b/docs/fr-rCA/user/messages-and-channels.md index 652114cdab..e1adb17cf5 100644 --- a/docs/fr-rCA/user/messages-and-channels.md +++ b/docs/fr-rCA/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/fr-rCA/user/mqtt.md b/docs/fr-rCA/user/mqtt.md index 951c3db90f..06b7a20068 100644 --- a/docs/fr-rCA/user/mqtt.md +++ b/docs/fr-rCA/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Désactivé | | **TLS enabled** | Secure connection to broker | Désactivé | | **Map reporting** | Report position to public map | Désactivé | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Désactivé | +| **Proxy to client enabled** | Relay MQTT through the connected app | Désactivé | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/fr-rCA/user/node-metrics.md b/docs/fr-rCA/user/node-metrics.md index 1d376c77be..d1d3339058 100644 --- a/docs/fr-rCA/user/node-metrics.md +++ b/docs/fr-rCA/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/fr-rCA/user/nodes.md b/docs/fr-rCA/user/nodes.md index 5f1e658a68..01ca6969a1 100644 --- a/docs/fr-rCA/user/nodes.md +++ b/docs/fr-rCA/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nœuds -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Capteur | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | Traqueur TAK | TAK position reporting only | -| Objets trouvés | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Objets trouvés | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtre | Description | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Description | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Description | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Dernière écoute | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distance | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/fr-rCA/user/notifications.md b/docs/fr-rCA/user/notifications.md new file mode 100644 index 0000000000..6b8dbde46f --- /dev/null +++ b/docs/fr-rCA/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Notifications de message | A message sent directly to you | The conversation | +| Messages | Diffuser les notifications de message | A message on one of your channels | The channel | +| Messages | Notifications de waypoint | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Notifications d'alerte | A critical alert from a node | The conversation | +| Maillage | Notifikasyon nouvo nœud | A node heard for the first time | The node's details | +| Maillage | Mesh invitation notifications | An invitation to join a nearby mesh | Découverte de maille locale | +| Maillage | Notifications de batterie faible (nœuds favoris) | A favorite node's battery running low | The node's details | +| Appareil | Notifications de service | The connection to your node while the app runs in the background | The app | +| Appareil | Notifications de batterie faible | Your node's battery running low | The node's details | +| Appareil | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Appareil | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/fr-rCA/user/onboarding.md b/docs/fr-rCA/user/onboarding.md index 43123e95c0..2ab8a2f0ba 100644 --- a/docs/fr-rCA/user/onboarding.md +++ b/docs/fr-rCA/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/fr-rCA/user/settings-module-admin.md b/docs/fr-rCA/user/settings-module-admin.md index e20ed54729..88d2695e5e 100644 --- a/docs/fr-rCA/user/settings-module-admin.md +++ b/docs/fr-rCA/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Configuration du module +## Réglages du module Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Sortie JSON activée | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS activé | Use secure connection | | Sujet principal | Base MQTT topic path | -| Proxy pour le client activé | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy pour le client activé | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Rapport cartographique | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Description | -| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| J'accepte. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervalle de rapport cartographique (secondes) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| J'accepte. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Délai d'expiration | How long to wait before considering an incoming message complete | | Outrepasser le port série de la console | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Description | -| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notifications externes activées | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED extérieure (GPIO) | Pin the LED is wired to | -| Sortie LED active à l’état haut | Whether the LED pin is active high or low | -| Buzzer extérieur (GPIO) | Pin the buzzer is wired to | -| Sortie vibreur (GPIO) | Pin the vibration motor is wired to | -| Utiliser le buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Utiliser l'I2S comme buzzer | Send the alert through an I2S audio output instead | -| Durée de sortie (en millisecondes) | How long a single alert lasts | -| Durée de répétition de la sortie (secondes) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Sonnerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Description | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| Notifications externes activées | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED extérieure (GPIO) | Pin the LED is wired to | +| Sortie LED active à l’état haut | Whether the LED pin is active high or low | +| Buzzer extérieur (GPIO) | Pin the buzzer is wired to | +| Sortie vibreur (GPIO) | Pin the vibration motor is wired to | +| Utiliser le buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Utiliser l'I2S comme buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Délai d'expiration du message | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Sonnerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Description | -| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Test de portée activé | Activate range testing | -| Intervalle de message de l'expéditeur (secondes) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Enregistrer .CSV dans le stockage (ESP32 seulement) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Test de portée activé | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Enregistrer .CSV dans le stockage (ESP32 seulement) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Description | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Envoyer la télémétrie de l'appareil | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Intervalle de mise à jour des mesures | How often to report battery, uptime and channel utilization | -| Module de métriques de l'environnement activé | Report the attached environment sensors | -| Intervalle de mise à jour des mesures d'environnement | How often to report them | -| Mesures d'environnement à l'écran activées | Also show these readings on the device's own display | -| Les mesures environnementales utilisent Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Module de mesure de la qualité de l'air activé | Report particulate and CO₂ sensor data | -| Intervalle de mise à jour des mesures de qualité d'air | How often to report them | -| Module de mesure de puissance activé | Report the per-channel voltage and current readings | -| Intervalle de mise à jour des mesures d'alimentation | How often to report them | -| Indicateurs d'alimentation à l'écran activés | Also show power readings on the device's display | +| Setting | Description | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Envoyer la télémétrie de l'appareil | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Intervalle de mise à jour des mesures | How often to report battery, uptime and channel utilization | +| Module de métriques de l'environnement activé | Report the attached environment sensors | +| Intervalle de mise à jour des mesures d'environnement | How often to report them | +| Mesures d'environnement à l'écran activées | Also show these readings on the device's own display | +| Les mesures environnementales utilisent Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Module de mesure de la qualité de l'air activé | Report particulate and CO₂ sensor data | +| Intervalle de mise à jour des mesures de qualité d'air | How often to report them | +| Module de mesure de puissance activé | Report the per-channel voltage and current readings | +| Intervalle de mise à jour des mesures d'alimentation | How often to report them | +| Indicateurs d'alimentation à l'écran activés | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Description | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Entrée Haut/Bas/Select activée | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Description | | ---------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Autoriser l'accès non défini aux broches | Allow access to any GPIO pin (security risk) | | Broches disponibles | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Description | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Infos de voisinage activées | Activate neighbor broadcasting | -| Intervalle de mise à jour (secondes) | How often to broadcast neighbor list | -| Transmettre par LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Infos de voisinage activées | Activate neighbor broadcasting | +| Fréquence de récupération GPS | How often to broadcast neighbor list | +| Transmettre par LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +215,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Actif | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -226,39 +225,39 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | Broche GPIO à surveiller | GPIO pin connected to sensor | | Type du déclencheur de détection | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | Utiliser le mode INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Diffusion minimale (secondes) | Minimum time between alert broadcasts | -| Diffusion de l'État (secondes) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Envoyer une sonnerie avec un message d'alerte | Include bell character in alerts | | Nom convivial | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Paramètre | Description | -| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter activé | Activate people counting | -| Intervalle de mise à jour (secondes) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Paramètre | Description | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter activé | Activate people counting | +| Fréquence de récupération GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Redémarrer | Restarts the radio | -| Éteindre | Powers the radio down | -| Réinitialisation d'usine | Returns every setting to its factory default | -| Reconfiguration de NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Redémarrer | Restarts the node | +| Éteindre | Powers the node down | +| Réinitialisation d'usine | Returns every setting to its factory default | +| Reconfiguration de NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Sauvegarder & Restaurer -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avancé **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Nettoyer la base de données des nœuds -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### A propros @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/fr-rCA/user/settings-radio-user.md b/docs/fr-rCA/user/settings-radio-user.md index eb4719523f..942b9de1d7 100644 --- a/docs/fr-rCA/user/settings-radio-user.md +++ b/docs/fr-rCA/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - paramètres - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Description | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nom long | Your display name (up to 39 characters) | -| Nom court | 4-character abbreviated name | -| Statut du message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Non joignable par message | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Radioamateur licencié (RA) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Description | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nom long | Your display name (up to 39 characters) | +| Nom court | 4-character abbreviated name | +| Statut du message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Non joignable par message | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Mode de réémission | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Intervalle de diffusion des infos nœud | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double clic comme appui sur le bouton | Treat a double tap as a button press | Désactivé | -| Triple clic pour faire un ping Ad Hoc | Send an ad-hoc position ping on a triple click | Désactivé | +| Triple clic pour faire un ping Ad Hoc | Send an ad-hoc position ping on a triple click | Activé | | LED de vérification de fonctionnement (heartbeat) | Blink the status LED periodically | Activé | | Fuseau horaire | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Description | Par défaut | -| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Préréglages | Speed/range tradeoff | LongFast | -| Nombre de sauts | Maximum retransmit hops | 3 | -| Puissance d'émission | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmission activée | Turning this off makes the node receive-only | On | -| Autoriser le dépassement du temps d'émission autorisé par heure | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Éteint | -| Ignorer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Transmission des paquets vers MQTT | Allow your packets to be forwarded to MQTT by gateways | Éteint | -| Gain RX Boosté | Extra receive gain on SX126x radios; costs a little current | Éteint | -| Ventilateur PA désactivé | Turn off the power-amplifier fan on hardware that has one | Éteint | +| Setting | Description | Par défaut | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Préréglages | Speed/range tradeoff | LongFast | +| Nombre de sauts | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmission activée | Turning this off makes the node receive-only | On | +| Autoriser le dépassement du temps d'émission autorisé par heure | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Éteint | +| Ignorer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Transmission des paquets vers MQTT | Allow your packets to be forwarded to MQTT by gateways | Éteint | +| Gain RX Boosté | Extra receive gain on SX126x radios; costs a little current | Éteint | +| Ventilateur PA désactivé | Turn off the power-amplifier fan on hardware that has one | Éteint | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configuration de l'écran -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Description | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Écran allumé pour | How long the display stays lit before sleeping | -| Intervalle du carrousel | How often the radio cycles between screens on its own | -| Mode d'affichage | Screen layout/density used by the firmware | -| Unités d'affichage | Metric or Imperial on the radio's screen | -| Utiliser le format horaire 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Titre en gras | Draw the screen's heading text in bold | -| Inverser l'écran | Rotate the display 180° for an inverted mounting | -| Type d'OLED | Auto, SSD1306, SH1106, SH1107 | -| Réveil par appui ou mouvement | Light the screen when the radio is tapped or moved | -| Orientation de la boussole | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Toujours pointer vers le nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Écran allumé pour | How long the display stays lit before sleeping | +| Intervalle du carrousel | How often the node cycles between screens on its own | +| Mode d'affichage | Screen layout/density used by the firmware | +| Unités d'affichage | Metric or Imperial on the node's screen | +| Utiliser le format horaire 12h | Show the node's clock as 12-hour rather than 24-hour | +| Titre en gras | Draw the screen's heading text in bold | +| Inverser l'écran | Rotate the display 180° for an inverted mounting | +| Type d'OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Réveil par appui ou mouvement | Light the screen when the node is tapped or moved | +| Orientation de la boussole | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Toujours pointer vers le nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configuration de la position On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Mode GPS (matériel physique) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Fréquence de récupération GPS | How often the radio asks its GPS for a fix | +| Fréquence de récupération GPS | How often the node asks its GPS for a fix | | Intervalle de diffusion | How often the position is shared with the mesh | | Position Intelligente | Broadcast based on movement rather than purely on the clock | | Intervalle intelligent | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Description | | ------------------------------------------------------- | --------------------------------------------------------------- | -| Activer le mode économie d'énergie | Let the radio sleep aggressively between activity | +| Activer le mode économie d'énergie | Let the node sleep aggressively between activity | | Arrêt en cas de perte d'alimentation | Power the device down after external power disappears | | Durée du sommeil extra profond | How long the deepest sleep state lasts | -| Durée minimale de réveil | The shortest time the radio stays awake once woken | +| Durée minimale de réveil | The shortest time the node stays awake once woken | | Durée d'attente max du Bluetooth | How long to wait for a phone to connect before sleeping | | Remplacer le multiplicateur ADC | Turn on a manual correction for battery-voltage readings | | Facteur de remplacement du multiplicateur ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configuration du réseau -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Mot de passe | Mot de passe du réseau | | Ethernet activé | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configuration Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Description | | ------------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Clé publique | Your node's public key (read-only) | | Clé Admin | Keys permitted to administer this node remotely — up to three | -| Clé privée | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Clé privée | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Régénérer la clé privée | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Console série | Serial console over the Stream API | -| API de journalisation de débogage activée | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Mode géré | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API de journalisation de débogage activée | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Mode géré | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Sauvegarder les clés | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/fr-rCA/user/signal-meter.md b/docs/fr-rCA/user/signal-meter.md index f66f81f52a..f7cbc473f1 100644 --- a/docs/fr-rCA/user/signal-meter.md +++ b/docs/fr-rCA/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/fr-rCA/user/tak.md b/docs/fr-rCA/user/tak.md index ed50f72cc9..e41b4b59d2 100644 --- a/docs/fr-rCA/user/tak.md +++ b/docs/fr-rCA/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/fr-rCA/user/telemetry-and-sensors.md b/docs/fr-rCA/user/telemetry-and-sensors.md index 1fe244efbb..27be95f990 100644 --- a/docs/fr-rCA/user/telemetry-and-sensors.md +++ b/docs/fr-rCA/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Capteur | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Capteur | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Capteur | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unité | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Poids | kg or lb | Card only — load cells, such as a beehive scale | -| Distance | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unité | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Poids | kg or lb | Card only — load cells, such as a beehive scale | +| Distance | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Métriques d'alimentation diff --git a/docs/fr-rCA/user/translate.md b/docs/fr-rCA/user/translate.md index 9965c9c73f..7b83d26272 100644 --- a/docs/fr-rCA/user/translate.md +++ b/docs/fr-rCA/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/fr-rCA/user/units-and-locale.md b/docs/fr-rCA/user/units-and-locale.md index dbb951a48a..7aa91d5feb 100644 --- a/docs/fr-rCA/user/units-and-locale.md +++ b/docs/fr-rCA/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/fr-rCA/user/widget.md b/docs/fr-rCA/user/widget.md index 4540fc8509..250d003e55 100644 --- a/docs/fr-rCA/user/widget.md +++ b/docs/fr-rCA/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Guide de l'utilisateur nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/fr-rFR/user/app-functions.md b/docs/fr-rFR/user/app-functions.md index 71221b6b58..2350b55875 100644 --- a/docs/fr-rFR/user/app-functions.md +++ b/docs/fr-rFR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: Guide de l'utilisateur nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/fr-rFR/user/connections.md b/docs/fr-rFR/user/connections.md index 6112d810bf..717c0a4506 100644 --- a/docs/fr-rFR/user/connections.md +++ b/docs/fr-rFR/user/connections.md @@ -1,8 +1,7 @@ --- title: Connexions -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Réseau | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/fr-rFR/user/debug-logs.md b/docs/fr-rFR/user/debug-logs.md index 5b710a0237..57cca67b13 100644 --- a/docs/fr-rFR/user/debug-logs.md +++ b/docs/fr-rFR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Guide de l'utilisateur nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/fr-rFR/user/desktop.md b/docs/fr-rFR/user/desktop.md index 114d3613be..83861e5803 100644 --- a/docs/fr-rFR/user/desktop.md +++ b/docs/fr-rFR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/fr-rFR/user/discovery.md b/docs/fr-rFR/user/discovery.md index 262207e631..bed9869206 100644 --- a/docs/fr-rFR/user/discovery.md +++ b/docs/fr-rFR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Découverte de maille locale -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Description | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informations sur les voisins @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/fr-rFR/user/firmware.md b/docs/fr-rFR/user/firmware.md index 3ae7ee1c68..26c8681f02 100644 --- a/docs/fr-rFR/user/firmware.md +++ b/docs/fr-rFR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/fr-rFR/user/help-and-docs.md b/docs/fr-rFR/user/help-and-docs.md index 3e47771be6..8604e8d0a5 100644 --- a/docs/fr-rFR/user/help-and-docs.md +++ b/docs/fr-rFR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Guide de l'utilisateur nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/fr-rFR/user/map-and-waypoints.md b/docs/fr-rFR/user/map-and-waypoints.md index 7c471e40cd..00afa292a5 100644 --- a/docs/fr-rFR/user/map-and-waypoints.md +++ b/docs/fr-rFR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Couches cartographiques -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/fr-rFR/user/messages-and-channels.md b/docs/fr-rFR/user/messages-and-channels.md index 652114cdab..e1adb17cf5 100644 --- a/docs/fr-rFR/user/messages-and-channels.md +++ b/docs/fr-rFR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/fr-rFR/user/mqtt.md b/docs/fr-rFR/user/mqtt.md index 951c3db90f..06b7a20068 100644 --- a/docs/fr-rFR/user/mqtt.md +++ b/docs/fr-rFR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Désactivé | | **TLS enabled** | Secure connection to broker | Désactivé | | **Map reporting** | Report position to public map | Désactivé | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Désactivé | +| **Proxy to client enabled** | Relay MQTT through the connected app | Désactivé | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/fr-rFR/user/node-metrics.md b/docs/fr-rFR/user/node-metrics.md index 1d376c77be..d1d3339058 100644 --- a/docs/fr-rFR/user/node-metrics.md +++ b/docs/fr-rFR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/fr-rFR/user/nodes.md b/docs/fr-rFR/user/nodes.md index 5f1e658a68..01ca6969a1 100644 --- a/docs/fr-rFR/user/nodes.md +++ b/docs/fr-rFR/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nœuds -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Capteur | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | Traqueur TAK | TAK position reporting only | -| Objets trouvés | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Objets trouvés | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtre | Description | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Description | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Description | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Dernière écoute | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distance | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/fr-rFR/user/notifications.md b/docs/fr-rFR/user/notifications.md new file mode 100644 index 0000000000..6b8dbde46f --- /dev/null +++ b/docs/fr-rFR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Notifications de message | A message sent directly to you | The conversation | +| Messages | Diffuser les notifications de message | A message on one of your channels | The channel | +| Messages | Notifications de waypoint | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Notifications d'alerte | A critical alert from a node | The conversation | +| Maillage | Notifikasyon nouvo nœud | A node heard for the first time | The node's details | +| Maillage | Mesh invitation notifications | An invitation to join a nearby mesh | Découverte de maille locale | +| Maillage | Notifications de batterie faible (nœuds favoris) | A favorite node's battery running low | The node's details | +| Appareil | Notifications de service | The connection to your node while the app runs in the background | The app | +| Appareil | Notifications de batterie faible | Your node's battery running low | The node's details | +| Appareil | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Appareil | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/fr-rFR/user/onboarding.md b/docs/fr-rFR/user/onboarding.md index 43123e95c0..2ab8a2f0ba 100644 --- a/docs/fr-rFR/user/onboarding.md +++ b/docs/fr-rFR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/fr-rFR/user/settings-module-admin.md b/docs/fr-rFR/user/settings-module-admin.md index e20ed54729..88d2695e5e 100644 --- a/docs/fr-rFR/user/settings-module-admin.md +++ b/docs/fr-rFR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Configuration du module +## Réglages du module Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Sortie JSON activée | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS activé | Use secure connection | | Sujet principal | Base MQTT topic path | -| Proxy pour le client activé | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy pour le client activé | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Rapport cartographique | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Description | -| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| J'accepte. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervalle de rapport cartographique (secondes) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| J'accepte. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Délai d'expiration | How long to wait before considering an incoming message complete | | Outrepasser le port série de la console | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Description | -| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notifications externes activées | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED extérieure (GPIO) | Pin the LED is wired to | -| Sortie LED active à l’état haut | Whether the LED pin is active high or low | -| Buzzer extérieur (GPIO) | Pin the buzzer is wired to | -| Sortie vibreur (GPIO) | Pin the vibration motor is wired to | -| Utiliser le buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Utiliser l'I2S comme buzzer | Send the alert through an I2S audio output instead | -| Durée de sortie (en millisecondes) | How long a single alert lasts | -| Durée de répétition de la sortie (secondes) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Sonnerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Description | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| Notifications externes activées | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED extérieure (GPIO) | Pin the LED is wired to | +| Sortie LED active à l’état haut | Whether the LED pin is active high or low | +| Buzzer extérieur (GPIO) | Pin the buzzer is wired to | +| Sortie vibreur (GPIO) | Pin the vibration motor is wired to | +| Utiliser le buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Utiliser l'I2S comme buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Délai d'expiration du message | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Sonnerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Description | -| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Test de portée activé | Activate range testing | -| Intervalle de message de l'expéditeur (secondes) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Enregistrer .CSV dans le stockage (ESP32 seulement) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Test de portée activé | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Enregistrer .CSV dans le stockage (ESP32 seulement) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Description | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Envoyer la télémétrie de l'appareil | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Intervalle de mise à jour des mesures | How often to report battery, uptime and channel utilization | -| Module de métriques de l'environnement activé | Report the attached environment sensors | -| Intervalle de mise à jour des mesures d'environnement | How often to report them | -| Mesures d'environnement à l'écran activées | Also show these readings on the device's own display | -| Les mesures environnementales utilisent Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Module de mesure de la qualité de l'air activé | Report particulate and CO₂ sensor data | -| Intervalle de mise à jour des mesures de qualité d'air | How often to report them | -| Module de mesure de puissance activé | Report the per-channel voltage and current readings | -| Intervalle de mise à jour des mesures d'alimentation | How often to report them | -| Indicateurs d'alimentation à l'écran activés | Also show power readings on the device's display | +| Setting | Description | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Envoyer la télémétrie de l'appareil | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Intervalle de mise à jour des mesures | How often to report battery, uptime and channel utilization | +| Module de métriques de l'environnement activé | Report the attached environment sensors | +| Intervalle de mise à jour des mesures d'environnement | How often to report them | +| Mesures d'environnement à l'écran activées | Also show these readings on the device's own display | +| Les mesures environnementales utilisent Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Module de mesure de la qualité de l'air activé | Report particulate and CO₂ sensor data | +| Intervalle de mise à jour des mesures de qualité d'air | How often to report them | +| Module de mesure de puissance activé | Report the per-channel voltage and current readings | +| Intervalle de mise à jour des mesures d'alimentation | How often to report them | +| Indicateurs d'alimentation à l'écran activés | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Description | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Entrée Haut/Bas/Select activée | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Description | | ---------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Autoriser l'accès non défini aux broches | Allow access to any GPIO pin (security risk) | | Broches disponibles | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Description | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Infos de voisinage activées | Activate neighbor broadcasting | -| Intervalle de mise à jour (secondes) | How often to broadcast neighbor list | -| Transmettre par LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Infos de voisinage activées | Activate neighbor broadcasting | +| Fréquence de récupération GPS | How often to broadcast neighbor list | +| Transmettre par LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +215,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Actif | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -226,39 +225,39 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | Broche GPIO à surveiller | GPIO pin connected to sensor | | Type du déclencheur de détection | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | Utiliser le mode INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Diffusion minimale (secondes) | Minimum time between alert broadcasts | -| Diffusion de l'État (secondes) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Envoyer une sonnerie avec un message d'alerte | Include bell character in alerts | | Nom convivial | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Paramètre | Description | -| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter activé | Activate people counting | -| Intervalle de mise à jour (secondes) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Paramètre | Description | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter activé | Activate people counting | +| Fréquence de récupération GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Redémarrer | Restarts the radio | -| Éteindre | Powers the radio down | -| Réinitialisation d'usine | Returns every setting to its factory default | -| Reconfiguration de NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Redémarrer | Restarts the node | +| Éteindre | Powers the node down | +| Réinitialisation d'usine | Returns every setting to its factory default | +| Reconfiguration de NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Sauvegarder & Restaurer -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avancé **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Nettoyer la base de données des nœuds -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### A propros @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/fr-rFR/user/settings-radio-user.md b/docs/fr-rFR/user/settings-radio-user.md index eb4719523f..942b9de1d7 100644 --- a/docs/fr-rFR/user/settings-radio-user.md +++ b/docs/fr-rFR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - paramètres - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Description | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nom long | Your display name (up to 39 characters) | -| Nom court | 4-character abbreviated name | -| Statut du message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Non joignable par message | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Radioamateur licencié (RA) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Description | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nom long | Your display name (up to 39 characters) | +| Nom court | 4-character abbreviated name | +| Statut du message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Non joignable par message | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Mode de réémission | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Intervalle de diffusion des infos nœud | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double clic comme appui sur le bouton | Treat a double tap as a button press | Désactivé | -| Triple clic pour faire un ping Ad Hoc | Send an ad-hoc position ping on a triple click | Désactivé | +| Triple clic pour faire un ping Ad Hoc | Send an ad-hoc position ping on a triple click | Activé | | LED de vérification de fonctionnement (heartbeat) | Blink the status LED periodically | Activé | | Fuseau horaire | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Description | Par défaut | -| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Préréglages | Speed/range tradeoff | LongFast | -| Nombre de sauts | Maximum retransmit hops | 3 | -| Puissance d'émission | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmission activée | Turning this off makes the node receive-only | On | -| Autoriser le dépassement du temps d'émission autorisé par heure | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Éteint | -| Ignorer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Transmission des paquets vers MQTT | Allow your packets to be forwarded to MQTT by gateways | Éteint | -| Gain RX Boosté | Extra receive gain on SX126x radios; costs a little current | Éteint | -| Ventilateur PA désactivé | Turn off the power-amplifier fan on hardware that has one | Éteint | +| Setting | Description | Par défaut | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Préréglages | Speed/range tradeoff | LongFast | +| Nombre de sauts | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmission activée | Turning this off makes the node receive-only | On | +| Autoriser le dépassement du temps d'émission autorisé par heure | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Éteint | +| Ignorer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Transmission des paquets vers MQTT | Allow your packets to be forwarded to MQTT by gateways | Éteint | +| Gain RX Boosté | Extra receive gain on SX126x radios; costs a little current | Éteint | +| Ventilateur PA désactivé | Turn off the power-amplifier fan on hardware that has one | Éteint | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configuration de l'écran -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Description | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Écran allumé pour | How long the display stays lit before sleeping | -| Intervalle du carrousel | How often the radio cycles between screens on its own | -| Mode d'affichage | Screen layout/density used by the firmware | -| Unités d'affichage | Metric or Imperial on the radio's screen | -| Utiliser le format horaire 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Titre en gras | Draw the screen's heading text in bold | -| Inverser l'écran | Rotate the display 180° for an inverted mounting | -| Type d'OLED | Auto, SSD1306, SH1106, SH1107 | -| Réveil par appui ou mouvement | Light the screen when the radio is tapped or moved | -| Orientation de la boussole | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Toujours pointer vers le nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Écran allumé pour | How long the display stays lit before sleeping | +| Intervalle du carrousel | How often the node cycles between screens on its own | +| Mode d'affichage | Screen layout/density used by the firmware | +| Unités d'affichage | Metric or Imperial on the node's screen | +| Utiliser le format horaire 12h | Show the node's clock as 12-hour rather than 24-hour | +| Titre en gras | Draw the screen's heading text in bold | +| Inverser l'écran | Rotate the display 180° for an inverted mounting | +| Type d'OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Réveil par appui ou mouvement | Light the screen when the node is tapped or moved | +| Orientation de la boussole | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Toujours pointer vers le nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configuration de la position On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Mode GPS (matériel physique) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Fréquence de récupération GPS | How often the radio asks its GPS for a fix | +| Fréquence de récupération GPS | How often the node asks its GPS for a fix | | Intervalle de diffusion | How often the position is shared with the mesh | | Position Intelligente | Broadcast based on movement rather than purely on the clock | | Intervalle intelligent | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Description | | ------------------------------------------------------- | --------------------------------------------------------------- | -| Activer le mode économie d'énergie | Let the radio sleep aggressively between activity | +| Activer le mode économie d'énergie | Let the node sleep aggressively between activity | | Arrêt en cas de perte d'alimentation | Power the device down after external power disappears | | Durée du sommeil extra profond | How long the deepest sleep state lasts | -| Durée minimale de réveil | The shortest time the radio stays awake once woken | +| Durée minimale de réveil | The shortest time the node stays awake once woken | | Durée d'attente max du Bluetooth | How long to wait for a phone to connect before sleeping | | Remplacer le multiplicateur ADC | Turn on a manual correction for battery-voltage readings | | Facteur de remplacement du multiplicateur ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configuration du réseau -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Mot de passe | Mot de passe du réseau | | Ethernet activé | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configuration Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Description | | ------------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Clé publique | Your node's public key (read-only) | | Clé Admin | Keys permitted to administer this node remotely — up to three | -| Clé privée | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Clé privée | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Régénérer la clé privée | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Console série | Serial console over the Stream API | -| API de journalisation de débogage activée | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Mode géré | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API de journalisation de débogage activée | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Mode géré | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Sauvegarder les clés | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/fr-rFR/user/signal-meter.md b/docs/fr-rFR/user/signal-meter.md index f66f81f52a..f7cbc473f1 100644 --- a/docs/fr-rFR/user/signal-meter.md +++ b/docs/fr-rFR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/fr-rFR/user/tak.md b/docs/fr-rFR/user/tak.md index ed50f72cc9..e41b4b59d2 100644 --- a/docs/fr-rFR/user/tak.md +++ b/docs/fr-rFR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/fr-rFR/user/telemetry-and-sensors.md b/docs/fr-rFR/user/telemetry-and-sensors.md index 1fe244efbb..27be95f990 100644 --- a/docs/fr-rFR/user/telemetry-and-sensors.md +++ b/docs/fr-rFR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Capteur | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Capteur | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Capteur | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unité | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Poids | kg or lb | Card only — load cells, such as a beehive scale | -| Distance | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unité | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Poids | kg or lb | Card only — load cells, such as a beehive scale | +| Distance | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Métriques d'alimentation diff --git a/docs/fr-rFR/user/translate.md b/docs/fr-rFR/user/translate.md index 9965c9c73f..7b83d26272 100644 --- a/docs/fr-rFR/user/translate.md +++ b/docs/fr-rFR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/fr-rFR/user/units-and-locale.md b/docs/fr-rFR/user/units-and-locale.md index dbb951a48a..7aa91d5feb 100644 --- a/docs/fr-rFR/user/units-and-locale.md +++ b/docs/fr-rFR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/fr-rFR/user/widget.md b/docs/fr-rFR/user/widget.md index 4540fc8509..250d003e55 100644 --- a/docs/fr-rFR/user/widget.md +++ b/docs/fr-rFR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Guide de l'utilisateur nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ga-rIE/user/app-functions.md b/docs/ga-rIE/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ga-rIE/user/app-functions.md +++ b/docs/ga-rIE/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ga-rIE/user/connections.md b/docs/ga-rIE/user/connections.md index cea04fbec5..3af58e7037 100644 --- a/docs/ga-rIE/user/connections.md +++ b/docs/ga-rIE/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ga-rIE/user/debug-logs.md b/docs/ga-rIE/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ga-rIE/user/debug-logs.md +++ b/docs/ga-rIE/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ga-rIE/user/desktop.md b/docs/ga-rIE/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/ga-rIE/user/desktop.md +++ b/docs/ga-rIE/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ga-rIE/user/discovery.md b/docs/ga-rIE/user/discovery.md index 3c9909287c..b340d1620d 100644 --- a/docs/ga-rIE/user/discovery.md +++ b/docs/ga-rIE/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Cur síos | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Cur síos | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ga-rIE/user/firmware.md b/docs/ga-rIE/user/firmware.md index f467df8786..54bd7501ae 100644 --- a/docs/ga-rIE/user/firmware.md +++ b/docs/ga-rIE/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ga-rIE/user/help-and-docs.md b/docs/ga-rIE/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ga-rIE/user/help-and-docs.md +++ b/docs/ga-rIE/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ga-rIE/user/map-and-waypoints.md b/docs/ga-rIE/user/map-and-waypoints.md index ca7948e013..3393bf42c7 100644 --- a/docs/ga-rIE/user/map-and-waypoints.md +++ b/docs/ga-rIE/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ga-rIE/user/messages-and-channels.md b/docs/ga-rIE/user/messages-and-channels.md index 2323d29a12..7597ebaf45 100644 --- a/docs/ga-rIE/user/messages-and-channels.md +++ b/docs/ga-rIE/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ga-rIE/user/mqtt.md b/docs/ga-rIE/user/mqtt.md index f15da334a3..7b4e38f8bd 100644 --- a/docs/ga-rIE/user/mqtt.md +++ b/docs/ga-rIE/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ga-rIE/user/node-metrics.md b/docs/ga-rIE/user/node-metrics.md index 9feac9a889..7dc3079999 100644 --- a/docs/ga-rIE/user/node-metrics.md +++ b/docs/ga-rIE/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ga-rIE/user/nodes.md b/docs/ga-rIE/user/nodes.md index b49f8e2588..674a55ad12 100644 --- a/docs/ga-rIE/user/nodes.md +++ b/docs/ga-rIE/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Scagaire | Cur síos | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Scagaire | Cur síos | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Cur síos | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Deiridh chluinmhu | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Sáth | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ga-rIE/user/notifications.md b/docs/ga-rIE/user/notifications.md new file mode 100644 index 0000000000..918a7fb33e --- /dev/null +++ b/docs/ga-rIE/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Fógartha faoi na nodes nua | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Fógraí seirbhíse | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ga-rIE/user/onboarding.md b/docs/ga-rIE/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/ga-rIE/user/onboarding.md +++ b/docs/ga-rIE/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ga-rIE/user/settings-module-admin.md b/docs/ga-rIE/user/settings-module-admin.md index f0f1e5a897..e0b151a8bc 100644 --- a/docs/ga-rIE/user/settings-module-admin.md +++ b/docs/ga-rIE/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Cumraíocht an mhódule Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Cur síos | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Cur síos | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Cur síos | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Cur síos | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Am tráth | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Cur síos | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Cur síos | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Cur síos | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Cur síos | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Cur síos | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Cur síos | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Cur síos | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Cur síos | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Cur síos | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Cur síos | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Cur síos | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Cur síos | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Cur síos | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Cur síos | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Athmhaoinigh | Restarts the radio | -| Dún | Powers the radio down | -| Athshocraigh an fhactaraí | Returns every setting to its factory default | -| Athshocraigh NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Athmhaoinigh | Restarts the node | +| Dún | Powers the node down | +| Athshocraigh an fhactaraí | Returns every setting to its factory default | +| Athshocraigh NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Maidir le @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ga-rIE/user/settings-radio-user.md b/docs/ga-rIE/user/settings-radio-user.md index 83ea0ddb47..c714b1815d 100644 --- a/docs/ga-rIE/user/settings-radio-user.md +++ b/docs/ga-rIE/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Cur síos | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Cur síos | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Cur síos | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Réigiún | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Cur síos | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Réigiún | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Cur síos | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Cur síos | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Cur síos | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Cur síos | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Cur síos | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Cur síos | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Cur síos | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ga-rIE/user/signal-meter.md b/docs/ga-rIE/user/signal-meter.md index 171b7273f5..05ce2069e3 100644 --- a/docs/ga-rIE/user/signal-meter.md +++ b/docs/ga-rIE/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ga-rIE/user/tak.md b/docs/ga-rIE/user/tak.md index 4f51aa709a..5be32923d9 100644 --- a/docs/ga-rIE/user/tak.md +++ b/docs/ga-rIE/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ga-rIE/user/telemetry-and-sensors.md b/docs/ga-rIE/user/telemetry-and-sensors.md index b49d964e34..a47cd603f2 100644 --- a/docs/ga-rIE/user/telemetry-and-sensors.md +++ b/docs/ga-rIE/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Sáth | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Sáth | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/ga-rIE/user/translate.md b/docs/ga-rIE/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/ga-rIE/user/translate.md +++ b/docs/ga-rIE/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ga-rIE/user/units-and-locale.md b/docs/ga-rIE/user/units-and-locale.md index 3a678b1a6f..65fed724fa 100644 --- a/docs/ga-rIE/user/units-and-locale.md +++ b/docs/ga-rIE/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ga-rIE/user/widget.md b/docs/ga-rIE/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ga-rIE/user/widget.md +++ b/docs/ga-rIE/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/gl-rES/user/app-functions.md b/docs/gl-rES/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/gl-rES/user/app-functions.md +++ b/docs/gl-rES/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/gl-rES/user/connections.md b/docs/gl-rES/user/connections.md index 86f4b5fe3d..cb2d62e12b 100644 --- a/docs/gl-rES/user/connections.md +++ b/docs/gl-rES/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/gl-rES/user/debug-logs.md b/docs/gl-rES/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/gl-rES/user/debug-logs.md +++ b/docs/gl-rES/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/gl-rES/user/desktop.md b/docs/gl-rES/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/gl-rES/user/desktop.md +++ b/docs/gl-rES/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/gl-rES/user/discovery.md b/docs/gl-rES/user/discovery.md index 039f03023e..632dc24a38 100644 --- a/docs/gl-rES/user/discovery.md +++ b/docs/gl-rES/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descrición | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descrición | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/gl-rES/user/firmware.md b/docs/gl-rES/user/firmware.md index 29be189d8e..03afa98002 100644 --- a/docs/gl-rES/user/firmware.md +++ b/docs/gl-rES/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/gl-rES/user/help-and-docs.md b/docs/gl-rES/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/gl-rES/user/help-and-docs.md +++ b/docs/gl-rES/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/gl-rES/user/map-and-waypoints.md b/docs/gl-rES/user/map-and-waypoints.md index 4b8d010093..b3ec0916dd 100644 --- a/docs/gl-rES/user/map-and-waypoints.md +++ b/docs/gl-rES/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/gl-rES/user/messages-and-channels.md b/docs/gl-rES/user/messages-and-channels.md index 9502ea77ff..40cd884c1b 100644 --- a/docs/gl-rES/user/messages-and-channels.md +++ b/docs/gl-rES/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/gl-rES/user/mqtt.md b/docs/gl-rES/user/mqtt.md index ab6d00f542..43c18a5264 100644 --- a/docs/gl-rES/user/mqtt.md +++ b/docs/gl-rES/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/gl-rES/user/node-metrics.md b/docs/gl-rES/user/node-metrics.md index 8f8423d758..4380811e55 100644 --- a/docs/gl-rES/user/node-metrics.md +++ b/docs/gl-rES/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/gl-rES/user/nodes.md b/docs/gl-rES/user/nodes.md index 58d981233e..8ff80fb8d2 100644 --- a/docs/gl-rES/user/nodes.md +++ b/docs/gl-rES/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtro | Descrición | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrición | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descrición | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Última escoita | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distancia | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/gl-rES/user/notifications.md b/docs/gl-rES/user/notifications.md new file mode 100644 index 0000000000..c44809b261 --- /dev/null +++ b/docs/gl-rES/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Notificacións de servizo | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/gl-rES/user/onboarding.md b/docs/gl-rES/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/gl-rES/user/onboarding.md +++ b/docs/gl-rES/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/gl-rES/user/settings-module-admin.md b/docs/gl-rES/user/settings-module-admin.md index 82e11a4b60..2f6d3aefad 100644 --- a/docs/gl-rES/user/settings-module-admin.md +++ b/docs/gl-rES/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configuración de módulo Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Descrición | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Descrición | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descrición | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descrición | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descrición | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descrición | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descrición | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descrición | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descrición | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Descrición | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descrición | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descrición | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descrición | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descrición | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Descrición | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Descrición | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descrición | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descrición | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reiniciar | Restarts the radio | -| Apagar | Powers the radio down | -| Restablecemento de fábrica | Returns every setting to its factory default | -| Restablecer NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reiniciar | Restarts the node | +| Apagar | Powers the node down | +| Restablecemento de fábrica | Returns every setting to its factory default | +| Restablecer NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Acerca de @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/gl-rES/user/settings-radio-user.md b/docs/gl-rES/user/settings-radio-user.md index db4c133ba8..06e08b76be 100644 --- a/docs/gl-rES/user/settings-radio-user.md +++ b/docs/gl-rES/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descrición | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descrición | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descrición | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rexión | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descrición | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Rexión | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descrición | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descrición | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrición | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descrición | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descrición | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descrición | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrición | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/gl-rES/user/signal-meter.md b/docs/gl-rES/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/gl-rES/user/signal-meter.md +++ b/docs/gl-rES/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/gl-rES/user/tak.md b/docs/gl-rES/user/tak.md index 5418db53e8..01f2472cb8 100644 --- a/docs/gl-rES/user/tak.md +++ b/docs/gl-rES/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/gl-rES/user/telemetry-and-sensors.md b/docs/gl-rES/user/telemetry-and-sensors.md index b16f68c0f6..d7491e882d 100644 --- a/docs/gl-rES/user/telemetry-and-sensors.md +++ b/docs/gl-rES/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distancia | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distancia | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/gl-rES/user/translate.md b/docs/gl-rES/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/gl-rES/user/translate.md +++ b/docs/gl-rES/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/gl-rES/user/units-and-locale.md b/docs/gl-rES/user/units-and-locale.md index a09eee3f53..8ce5008e4a 100644 --- a/docs/gl-rES/user/units-and-locale.md +++ b/docs/gl-rES/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/gl-rES/user/widget.md b/docs/gl-rES/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/gl-rES/user/widget.md +++ b/docs/gl-rES/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/hr-rHR/user/app-functions.md b/docs/hr-rHR/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/hr-rHR/user/app-functions.md +++ b/docs/hr-rHR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/hr-rHR/user/connections.md b/docs/hr-rHR/user/connections.md index 2aa28e91d2..0c36c81690 100644 --- a/docs/hr-rHR/user/connections.md +++ b/docs/hr-rHR/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/hr-rHR/user/debug-logs.md b/docs/hr-rHR/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/hr-rHR/user/debug-logs.md +++ b/docs/hr-rHR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/hr-rHR/user/desktop.md b/docs/hr-rHR/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/hr-rHR/user/desktop.md +++ b/docs/hr-rHR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/hr-rHR/user/discovery.md b/docs/hr-rHR/user/discovery.md index f095744ec9..57adf5f084 100644 --- a/docs/hr-rHR/user/discovery.md +++ b/docs/hr-rHR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Opis | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Opis | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/hr-rHR/user/firmware.md b/docs/hr-rHR/user/firmware.md index 2daac0cc59..d442b3f14c 100644 --- a/docs/hr-rHR/user/firmware.md +++ b/docs/hr-rHR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/hr-rHR/user/help-and-docs.md b/docs/hr-rHR/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/hr-rHR/user/help-and-docs.md +++ b/docs/hr-rHR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/hr-rHR/user/map-and-waypoints.md b/docs/hr-rHR/user/map-and-waypoints.md index af0a337e11..b7b6796e1b 100644 --- a/docs/hr-rHR/user/map-and-waypoints.md +++ b/docs/hr-rHR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/hr-rHR/user/messages-and-channels.md b/docs/hr-rHR/user/messages-and-channels.md index 1936a29ee7..e16ccdbc50 100644 --- a/docs/hr-rHR/user/messages-and-channels.md +++ b/docs/hr-rHR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/hr-rHR/user/mqtt.md b/docs/hr-rHR/user/mqtt.md index b76237c878..56e2848252 100644 --- a/docs/hr-rHR/user/mqtt.md +++ b/docs/hr-rHR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/hr-rHR/user/node-metrics.md b/docs/hr-rHR/user/node-metrics.md index 09910afb78..1326df7308 100644 --- a/docs/hr-rHR/user/node-metrics.md +++ b/docs/hr-rHR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/hr-rHR/user/nodes.md b/docs/hr-rHR/user/nodes.md index 513da64c12..e82208141f 100644 --- a/docs/hr-rHR/user/nodes.md +++ b/docs/hr-rHR/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtriraj | Opis | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtriraj | Opis | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Opis | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Posljednje čuo | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Udaljenost | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/hr-rHR/user/notifications.md b/docs/hr-rHR/user/notifications.md new file mode 100644 index 0000000000..9a3b64f3a5 --- /dev/null +++ b/docs/hr-rHR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Servisne obavijesti | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/hr-rHR/user/onboarding.md b/docs/hr-rHR/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/hr-rHR/user/onboarding.md +++ b/docs/hr-rHR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/hr-rHR/user/settings-module-admin.md b/docs/hr-rHR/user/settings-module-admin.md index 190f63c959..c411306e92 100644 --- a/docs/hr-rHR/user/settings-module-admin.md +++ b/docs/hr-rHR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Konfiguracija modula Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Opis | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Opis | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Opis | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Opis | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Opis | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Opis | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Opis | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Opis | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Opis | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Opis | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Opis | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Opis | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Opis | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Opis | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Opis | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Opis | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Opis | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Opis | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------------ | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Ponovno pokreni | Restarts the radio | -| Isključi | Powers the radio down | -| Vraćanje na tvorničke postavke | Returns every setting to its factory default | -| Resetiraj NodeDB bazu | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------------ | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Ponovno pokreni | Restarts the node | +| Isključi | Powers the node down | +| Vraćanje na tvorničke postavke | Returns every setting to its factory default | +| Resetiraj NodeDB bazu | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O programu @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/hr-rHR/user/settings-radio-user.md b/docs/hr-rHR/user/settings-radio-user.md index 2675696dcc..d34438a15b 100644 --- a/docs/hr-rHR/user/settings-radio-user.md +++ b/docs/hr-rHR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Opis | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Opis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Opis | Zadano | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Opis | Zadano | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Opis | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Opis | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Opis | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Opis | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Postavke Bluetootha -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Opis | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/hr-rHR/user/signal-meter.md b/docs/hr-rHR/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/hr-rHR/user/signal-meter.md +++ b/docs/hr-rHR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/hr-rHR/user/tak.md b/docs/hr-rHR/user/tak.md index 1bc2ac7f03..a3486964dc 100644 --- a/docs/hr-rHR/user/tak.md +++ b/docs/hr-rHR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/hr-rHR/user/telemetry-and-sensors.md b/docs/hr-rHR/user/telemetry-and-sensors.md index 27ccfabae2..b67b3ecd0c 100644 --- a/docs/hr-rHR/user/telemetry-and-sensors.md +++ b/docs/hr-rHR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Udaljenost | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Udaljenost | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/hr-rHR/user/translate.md b/docs/hr-rHR/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/hr-rHR/user/translate.md +++ b/docs/hr-rHR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/hr-rHR/user/units-and-locale.md b/docs/hr-rHR/user/units-and-locale.md index a09eee3f53..8ce5008e4a 100644 --- a/docs/hr-rHR/user/units-and-locale.md +++ b/docs/hr-rHR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/hr-rHR/user/widget.md b/docs/hr-rHR/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/hr-rHR/user/widget.md +++ b/docs/hr-rHR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ht-rHT/user/app-functions.md b/docs/ht-rHT/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ht-rHT/user/app-functions.md +++ b/docs/ht-rHT/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ht-rHT/user/connections.md b/docs/ht-rHT/user/connections.md index 802a47059b..8af95f3e2d 100644 --- a/docs/ht-rHT/user/connections.md +++ b/docs/ht-rHT/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ht-rHT/user/debug-logs.md b/docs/ht-rHT/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ht-rHT/user/debug-logs.md +++ b/docs/ht-rHT/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ht-rHT/user/desktop.md b/docs/ht-rHT/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/ht-rHT/user/desktop.md +++ b/docs/ht-rHT/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ht-rHT/user/discovery.md b/docs/ht-rHT/user/discovery.md index 12edafcbe7..fc6ad2b015 100644 --- a/docs/ht-rHT/user/discovery.md +++ b/docs/ht-rHT/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Deskripsyon | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Deskripsyon | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ht-rHT/user/firmware.md b/docs/ht-rHT/user/firmware.md index 3888e74d15..b001230320 100644 --- a/docs/ht-rHT/user/firmware.md +++ b/docs/ht-rHT/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ht-rHT/user/help-and-docs.md b/docs/ht-rHT/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ht-rHT/user/help-and-docs.md +++ b/docs/ht-rHT/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ht-rHT/user/map-and-waypoints.md b/docs/ht-rHT/user/map-and-waypoints.md index 024ec5d7b5..cefd241501 100644 --- a/docs/ht-rHT/user/map-and-waypoints.md +++ b/docs/ht-rHT/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ht-rHT/user/messages-and-channels.md b/docs/ht-rHT/user/messages-and-channels.md index 2db2eb8b75..a784e6c944 100644 --- a/docs/ht-rHT/user/messages-and-channels.md +++ b/docs/ht-rHT/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ht-rHT/user/mqtt.md b/docs/ht-rHT/user/mqtt.md index b6c1d86644..920010f771 100644 --- a/docs/ht-rHT/user/mqtt.md +++ b/docs/ht-rHT/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ht-rHT/user/node-metrics.md b/docs/ht-rHT/user/node-metrics.md index a19011fd46..5e5711d74d 100644 --- a/docs/ht-rHT/user/node-metrics.md +++ b/docs/ht-rHT/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ht-rHT/user/nodes.md b/docs/ht-rHT/user/nodes.md index 21a368f0cb..748c909f89 100644 --- a/docs/ht-rHT/user/nodes.md +++ b/docs/ht-rHT/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtre | Deskripsyon | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Deskripsyon | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Deskripsyon | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Dènye fwa li tande | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distans | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ht-rHT/user/notifications.md b/docs/ht-rHT/user/notifications.md new file mode 100644 index 0000000000..7141722b9e --- /dev/null +++ b/docs/ht-rHT/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Notifikasyon nouvo nœud | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Notifikasyon sèvis | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ht-rHT/user/onboarding.md b/docs/ht-rHT/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/ht-rHT/user/onboarding.md +++ b/docs/ht-rHT/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ht-rHT/user/settings-module-admin.md b/docs/ht-rHT/user/settings-module-admin.md index 25ead49c9d..ce502aa96f 100644 --- a/docs/ht-rHT/user/settings-module-admin.md +++ b/docs/ht-rHT/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Konfigirasyon modil Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Deskripsyon | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Deskripsyon | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Deskripsyon | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Deskripsyon | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Tan pase | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Deskripsyon | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Deskripsyon | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Deskripsyon | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Deskripsyon | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Deskripsyon | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Deskripsyon | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Deskripsyon | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Deskripsyon | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Deskripsyon | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Deskripsyon | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Deskripsyon | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Deskripsyon | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Deskripsyon | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Deskripsyon | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| --------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Rekòmanse | Restarts the radio | -| Fèmen | Powers the radio down | -| Reyajiste nan faktori | Returns every setting to its factory default | -| Reyajiste NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| --------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Rekòmanse | Restarts the node | +| Fèmen | Powers the node down | +| Reyajiste nan faktori | Returns every setting to its factory default | +| Reyajiste NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Sou @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ht-rHT/user/settings-radio-user.md b/docs/ht-rHT/user/settings-radio-user.md index f26ef53f65..76ae52fda7 100644 --- a/docs/ht-rHT/user/settings-radio-user.md +++ b/docs/ht-rHT/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Deskripsyon | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Deskripsyon | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Deskripsyon | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rejyon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Deskripsyon | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Rejyon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Deskripsyon | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Deskripsyon | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Deskripsyon | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Deskripsyon | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Deskripsyon | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Deskripsyon | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Deskripsyon | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ht-rHT/user/signal-meter.md b/docs/ht-rHT/user/signal-meter.md index 3296b03565..f27beab580 100644 --- a/docs/ht-rHT/user/signal-meter.md +++ b/docs/ht-rHT/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ht-rHT/user/tak.md b/docs/ht-rHT/user/tak.md index 0a9dff760d..349f4cb8c0 100644 --- a/docs/ht-rHT/user/tak.md +++ b/docs/ht-rHT/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ht-rHT/user/telemetry-and-sensors.md b/docs/ht-rHT/user/telemetry-and-sensors.md index a354bed193..a7f1f150e8 100644 --- a/docs/ht-rHT/user/telemetry-and-sensors.md +++ b/docs/ht-rHT/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distans | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distans | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/ht-rHT/user/translate.md b/docs/ht-rHT/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/ht-rHT/user/translate.md +++ b/docs/ht-rHT/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ht-rHT/user/units-and-locale.md b/docs/ht-rHT/user/units-and-locale.md index 8188fc194e..96b21a46df 100644 --- a/docs/ht-rHT/user/units-and-locale.md +++ b/docs/ht-rHT/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ht-rHT/user/widget.md b/docs/ht-rHT/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ht-rHT/user/widget.md +++ b/docs/ht-rHT/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/hu-rHU/user/app-functions.md b/docs/hu-rHU/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/hu-rHU/user/app-functions.md +++ b/docs/hu-rHU/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/hu-rHU/user/connections.md b/docs/hu-rHU/user/connections.md index 37dc316549..005faea97d 100644 --- a/docs/hu-rHU/user/connections.md +++ b/docs/hu-rHU/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Hálózat | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/hu-rHU/user/debug-logs.md b/docs/hu-rHU/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/hu-rHU/user/debug-logs.md +++ b/docs/hu-rHU/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/hu-rHU/user/desktop.md b/docs/hu-rHU/user/desktop.md index f9cb686b4c..54978de8a4 100644 --- a/docs/hu-rHU/user/desktop.md +++ b/docs/hu-rHU/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/hu-rHU/user/discovery.md b/docs/hu-rHU/user/discovery.md index 2f3d3b9c5e..0d9698df64 100644 --- a/docs/hu-rHU/user/discovery.md +++ b/docs/hu-rHU/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Leírás | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Leírás | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Szomszéd-információ @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/hu-rHU/user/firmware.md b/docs/hu-rHU/user/firmware.md index b2073e38ef..2d321af942 100644 --- a/docs/hu-rHU/user/firmware.md +++ b/docs/hu-rHU/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/hu-rHU/user/help-and-docs.md b/docs/hu-rHU/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/hu-rHU/user/help-and-docs.md +++ b/docs/hu-rHU/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/hu-rHU/user/map-and-waypoints.md b/docs/hu-rHU/user/map-and-waypoints.md index 4a9dcfa916..6795314e1b 100644 --- a/docs/hu-rHU/user/map-and-waypoints.md +++ b/docs/hu-rHU/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Térképrétegek -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/hu-rHU/user/messages-and-channels.md b/docs/hu-rHU/user/messages-and-channels.md index 859e15a40a..66f0827221 100644 --- a/docs/hu-rHU/user/messages-and-channels.md +++ b/docs/hu-rHU/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/hu-rHU/user/mqtt.md b/docs/hu-rHU/user/mqtt.md index ed35339d9a..c8d71a0053 100644 --- a/docs/hu-rHU/user/mqtt.md +++ b/docs/hu-rHU/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/hu-rHU/user/node-metrics.md b/docs/hu-rHU/user/node-metrics.md index fb3299f6da..81790f95cd 100644 --- a/docs/hu-rHU/user/node-metrics.md +++ b/docs/hu-rHU/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/hu-rHU/user/nodes.md b/docs/hu-rHU/user/nodes.md index efe82de447..8b9bd6ef8e 100644 --- a/docs/hu-rHU/user/nodes.md +++ b/docs/hu-rHU/user/nodes.md @@ -1,8 +1,7 @@ --- title: Csomópontok -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Szenzor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Elveszett és Megkerült | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Elveszett és Megkerült | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Leírás | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Leírás | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Leírás | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Utoljára hallott | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Távolság | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/hu-rHU/user/notifications.md b/docs/hu-rHU/user/notifications.md new file mode 100644 index 0000000000..d9b713db2d --- /dev/null +++ b/docs/hu-rHU/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Üzenetek | Közvetlen üzenet értesítések | A message sent directly to you | The conversation | +| Üzenetek | Sugárzott üzenet értesítések | A message on one of your channels | The channel | +| Üzenetek | Útpont-értesítések | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Üzenetek | Riasztási értesítések | A critical alert from a node | The conversation | +| Mesh | Új állomás értesítések | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Alacsony töltöttségű értesítések (kedvenc csomópontok) | A favorite node's battery running low | The node's details | +| Eszköz | Szolgáltatás értesítések | The connection to your node while the app runs in the background | The app | +| Eszköz | Alacsony töltöttség értesítések | Your node's battery running low | The node's details | +| Eszköz | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Eszköz | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/hu-rHU/user/onboarding.md b/docs/hu-rHU/user/onboarding.md index d4654bbdf9..36273ae2b7 100644 --- a/docs/hu-rHU/user/onboarding.md +++ b/docs/hu-rHU/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/hu-rHU/user/settings-module-admin.md b/docs/hu-rHU/user/settings-module-admin.md index 25a244308d..e5bd4a4f6f 100644 --- a/docs/hu-rHU/user/settings-module-admin.md +++ b/docs/hu-rHU/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Modul beállítások Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Leírás | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON kimenet engedélyezve | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS engedélyezve | Use secure connection | | Gyökér téma | Base MQTT topic path | -| Proxy kliens felé engedélyezve | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy kliens felé engedélyezve | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Térképadat-jelentés | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Leírás | -| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Elfogadom. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Térképadat-jelentés intervalluma (másodperc) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Leírás | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Elfogadom. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Időtúllépés | How long to wait before considering an incoming message complete | | Konzol soros port felülbírálása | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Leírás | -| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Külső értesítés engedélyezve | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Kimeneti LED (GPIO) | Pin the LED is wired to | -| Kimeneti LED aktív magas szint | Whether the LED pin is active high or low | -| Kimeneti csipogó (GPIO) | Pin the buzzer is wired to | -| Kimeneti rezgő (GPIO) | Pin the vibration motor is wired to | -| PWM csipogó használata | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| I2S használata csipogóként | Send the alert through an I2S audio output instead | -| Kimeneti időtartam (ezredmásodperc) | How long a single alert lasts | -| Ismétlő riasztás időkorlát (másodperc) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Csengőhang | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Leírás | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| Külső értesítés engedélyezve | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Kimeneti LED (GPIO) | Pin the LED is wired to | +| Kimeneti LED aktív magas szint | Whether the LED pin is active high or low | +| Kimeneti csipogó (GPIO) | Pin the buzzer is wired to | +| Kimeneti rezgő (GPIO) | Pin the vibration motor is wired to | +| PWM csipogó használata | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| I2S használata csipogóként | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Csengőhang | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Leírás | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| Hatótáv-teszt engedélyezve | Activate range testing | -| Küldési üzenetintervallum (másodperc) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| .CSV mentése a tárhelyre (csak ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Leírás | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| Hatótáv-teszt engedélyezve | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| .CSV mentése a tárhelyre (csak ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Leírás | -| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Eszköztelemetria küldése | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Eszközmetrikák frissítési időköze | How often to report battery, uptime and channel utilization | -| Környezeti metrika modul engedélyezve | Report the attached environment sensors | -| Környezeti metrikák frissítési időköze | How often to report them | -| Környezeti metrikák megjelenítése képernyőn | Also show these readings on the device's own display | -| Környezeti metrikák Fahrenheit-ben | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Levegőminőség-metrika modul engedélyezve | Report particulate and CO₂ sensor data | -| Levegőminőségi metrikák frissítési időköze | How often to report them | -| Energia-metrika modul engedélyezve | Report the per-channel voltage and current readings | -| Tápellátási metrikák frissítési időköze | How often to report them | -| Energia-metrikák megjelenítése képernyőn engedélyezve | Also show power readings on the device's display | +| Setting | Leírás | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Eszköztelemetria küldése | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Eszközmetrikák frissítési időköze | How often to report battery, uptime and channel utilization | +| Környezeti metrika modul engedélyezve | Report the attached environment sensors | +| Környezeti metrikák frissítési időköze | How often to report them | +| Környezeti metrikák megjelenítése képernyőn | Also show these readings on the device's own display | +| Környezeti metrikák Fahrenheit-ben | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Levegőminőség-metrika modul engedélyezve | Report particulate and CO₂ sensor data | +| Levegőminőségi metrikák frissítési időköze | How often to report them | +| Energia-metrika modul engedélyezve | Report the per-channel voltage and current readings | +| Tápellátási metrikák frissítési időköze | How often to report them | +| Energia-metrikák megjelenítése képernyőn engedélyezve | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Leírás | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Fel/Le/Kiválaszt gomb engedélyezve | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Leírás | | -------------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Nem definiált pinek elérésének engedélyezése | Allow access to any GPIO pin (security risk) | | Elérhető pinek | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Leírás | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Szomszéd-információ engedélyezve | Activate neighbor broadcasting | -| Frissítési intervallum (másodperc) | How often to broadcast neighbor list | -| Továbbítás LoRa-n keresztül | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Leírás | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Szomszéd-információ engedélyezve | Activate neighbor broadcasting | +| Frissítési időköz | How often to broadcast neighbor list | +| Továbbítás LoRa-n keresztül | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Áramerősség | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Leírás | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| Érzékelő szenzor engedélyezve | Activate detection sensor | -| Figyelt GPIO láb | GPIO pin connected to sensor | -| Érzékelési ravasztípus | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| INPUT_PULLUP mód használata | Enable the pin's internal pull-up resistor | -| Minimális sugárzási idő (másodperc) | Minimum time between alert broadcasts | -| Állapot-sugárzás (másodperc) | Periodic state broadcast interval | -| Harangjel küldése riasztási üzenettel | Include bell character in alerts | -| Barátságos név | Custom name for this sensor | +| Setting | Leírás | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Érzékelő szenzor engedélyezve | Activate detection sensor | +| Figyelt GPIO láb | GPIO pin connected to sensor | +| Érzékelési ravasztípus | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| INPUT_PULLUP mód használata | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Harangjel küldése riasztási üzenettel | Include bell character in alerts | +| Barátságos név | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Leírás | -| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter engedélyezve | Activate people counting | -| Frissítési intervallum (másodperc) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Leírás | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter engedélyezve | Activate people counting | +| Frissítési időköz | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Újraindítás | Restarts the radio | -| Leállítás | Powers the radio down | -| Gyári beállítások visszaállítása | Returns every setting to its factory default | -| NodeDB törlése | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Újraindítás | Restarts the node | +| Leállítás | Powers the node down | +| Gyári beállítások visszaállítása | Returns every setting to its factory default | +| NodeDB törlése | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Biztonsági mentés és visszaállítás -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Haladó **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Csomópont-adatbázis tisztítása -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### A programról @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/hu-rHU/user/settings-radio-user.md b/docs/hu-rHU/user/settings-radio-user.md index 17ab5d88bf..006412d1fb 100644 --- a/docs/hu-rHU/user/settings-radio-user.md +++ b/docs/hu-rHU/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - beállítások - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Leírás | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Hosszú név | Your display name (up to 39 characters) | -| Rövid név | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Nem üzenetképes | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Leírás | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hosszú név | Your display name (up to 39 characters) | +| Rövid név | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Nem üzenetképes | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Újrasugárzási mód | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Csomópont-információ sugárzási időköze | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Dupla koppintás mint gomb | Treat a double tap as a button press | Disabled | -| Háromszori kattintás – ad hoc ping | Send an ad-hoc position ping on a triple click | Disabled | +| Háromszori kattintás – ad hoc ping | Send an ad-hoc position ping on a triple click | Engedélyezve | | LED ütemjelzés | Blink the status LED periodically | Engedélyezve | | Időzóna | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Leírás | Alapértelmezett | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Régió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Előbeállítások | Speed/range tradeoff | LongFast | -| Ugrások száma | Maximum retransmit hops | 3 | -| Adásteljesítmény | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frekvencia felülbírálása | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Előbeállítás használata | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Szórási Faktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Kódolási ráta | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Sávszélesség | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frekvencia sáv | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Adás engedélyezve | Turning this off makes the node receive-only | On | -| Duty Cycle felülbírálása | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| MQTT figyelmen kívül hagyása | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| MQTT-re továbbítható | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX fokozott erősítés | Extra receive gain on SX126x radios; costs a little current | Off | -| PA ventilátor letiltva | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Leírás | Alapértelmezett | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Régió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Előbeállítások | Speed/range tradeoff | LongFast | +| Ugrások száma | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frekvencia felülbírálása | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Előbeállítás használata | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Szórási Faktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Kódolási ráta | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Sávszélesség | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frekvencia sáv | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Adás engedélyezve | Turning this off makes the node receive-only | On | +| Duty Cycle felülbírálása | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| MQTT figyelmen kívül hagyása | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| MQTT-re továbbítható | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX fokozott erősítés | Extra receive gain on SX126x radios; costs a little current | Off | +| PA ventilátor letiltva | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Kijelző beállítások -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Leírás | -| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Kijelző bekapcsolva ennyi ideig | How long the display stays lit before sleeping | -| Karusszel időköz | How often the radio cycles between screens on its own | -| Kijelző mód | Screen layout/density used by the firmware | -| Mértékegységek megjelenítése | Metric or Imperial on the radio's screen | -| 12 órás időformátum használata | Show the radio's clock as 12-hour rather than 24-hour | -| Félkövér címsor | Draw the screen's heading text in bold | -| Kijelző megfordítása | Rotate the display 180° for an inverted mounting | -| OLED típus | Auto, SSD1306, SH1106, SH1107 | -| Érintésre vagy mozgásra ébresztés | Light the screen when the radio is tapped or moved | -| Iránytű tájolás | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Mindig észak felé mutasson | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Leírás | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Kijelző bekapcsolva ennyi ideig | How long the display stays lit before sleeping | +| Karusszel időköz | How often the node cycles between screens on its own | +| Kijelző mód | Screen layout/density used by the firmware | +| Mértékegységek megjelenítése | Metric or Imperial on the node's screen | +| 12 órás időformátum használata | Show the node's clock as 12-hour rather than 24-hour | +| Félkövér címsor | Draw the screen's heading text in bold | +| Kijelző megfordítása | Rotate the display 180° for an inverted mounting | +| OLED típus | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Érintésre vagy mozgásra ébresztés | Light the screen when the node is tapped or moved | +| Iránytű tájolás | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Mindig észak felé mutasson | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Pozíció beállítások On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Leírás | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS mód (fizikai hardver) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Sugárzási időköz | How often the position is shared with the mesh | | Intelligens pozíció | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Leírás | | ------------------------------------------------ | --------------------------------------------------------------- | -| Energiatakarékos mód engedélyezése | Let the radio sleep aggressively between activity | +| Energiatakarékos mód engedélyezése | Let the node sleep aggressively between activity | | Leállítás áramszünet esetén | Power the device down after external power disappears | | Szuper mélyalvás időtartama | How long the deepest sleep state lasts | -| Minimális ébrenléti idő | The shortest time the radio stays awake once woken | +| Minimális ébrenléti idő | The shortest time the node stays awake once woken | | Bluetooth-várakozás időtartama | How long to wait for a phone to connect before sleeping | | ADC szorzó felülbírálása | Turn on a manual correction for battery-voltage readings | | ADC szorzó felülbírálási arány | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Hálózati beállítások -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Leírás | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Jelszó | Network password | | Ethernet engedélyezve | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth beállítások -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Leírás | | ---------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Leírás | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Nyilvános kulcs | Your node's public key (read-only) | | Admin kulcs | Keys permitted to administer this node remotely — up to three | -| Privát kulcs | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privát kulcs | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Privát kulcs újragenerálása | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Soros konzol | Serial console over the Stream API | -| Hibakeresési napló API engedélyezve | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Felügyelt mód | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Hibakeresési napló API engedélyezve | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Felügyelt mód | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/hu-rHU/user/signal-meter.md b/docs/hu-rHU/user/signal-meter.md index 8505c11a5e..39df8280ec 100644 --- a/docs/hu-rHU/user/signal-meter.md +++ b/docs/hu-rHU/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/hu-rHU/user/tak.md b/docs/hu-rHU/user/tak.md index 7325f1496b..fb593687f8 100644 --- a/docs/hu-rHU/user/tak.md +++ b/docs/hu-rHU/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/hu-rHU/user/telemetry-and-sensors.md b/docs/hu-rHU/user/telemetry-and-sensors.md index 1095b059a1..acf2325743 100644 --- a/docs/hu-rHU/user/telemetry-and-sensors.md +++ b/docs/hu-rHU/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Szenzor | Metric | Jegyzetek | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Szenzor | Metric | Jegyzetek | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Szenzor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Sugárzás | µR/h | Card and chart | -| Súly | kg or lb | Card only — load cells, such as a beehive scale | -| Távolság | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Sugárzás | µR/h | Card and chart | +| Súly | kg or lb | Card only — load cells, such as a beehive scale | +| Távolság | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Tápellátási metrikák diff --git a/docs/hu-rHU/user/translate.md b/docs/hu-rHU/user/translate.md index aa56a8df4d..41513d80ee 100644 --- a/docs/hu-rHU/user/translate.md +++ b/docs/hu-rHU/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/hu-rHU/user/units-and-locale.md b/docs/hu-rHU/user/units-and-locale.md index 0ec9bc9fc0..dfe2484b48 100644 --- a/docs/hu-rHU/user/units-and-locale.md +++ b/docs/hu-rHU/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/hu-rHU/user/widget.md b/docs/hu-rHU/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/hu-rHU/user/widget.md +++ b/docs/hu-rHU/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/is-rIS/user/app-functions.md b/docs/is-rIS/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/is-rIS/user/app-functions.md +++ b/docs/is-rIS/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/is-rIS/user/connections.md b/docs/is-rIS/user/connections.md index 612c7e1a7c..632c38f0d7 100644 --- a/docs/is-rIS/user/connections.md +++ b/docs/is-rIS/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/is-rIS/user/debug-logs.md b/docs/is-rIS/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/is-rIS/user/debug-logs.md +++ b/docs/is-rIS/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/is-rIS/user/desktop.md b/docs/is-rIS/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/is-rIS/user/desktop.md +++ b/docs/is-rIS/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/is-rIS/user/discovery.md b/docs/is-rIS/user/discovery.md index 4b9a071eb5..075f3ec25c 100644 --- a/docs/is-rIS/user/discovery.md +++ b/docs/is-rIS/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Lýsing | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Lýsing | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/is-rIS/user/firmware.md b/docs/is-rIS/user/firmware.md index d5dc2e52bf..28deed4678 100644 --- a/docs/is-rIS/user/firmware.md +++ b/docs/is-rIS/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/is-rIS/user/help-and-docs.md b/docs/is-rIS/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/is-rIS/user/help-and-docs.md +++ b/docs/is-rIS/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/is-rIS/user/map-and-waypoints.md b/docs/is-rIS/user/map-and-waypoints.md index c162477c3e..016ce5ff21 100644 --- a/docs/is-rIS/user/map-and-waypoints.md +++ b/docs/is-rIS/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/is-rIS/user/messages-and-channels.md b/docs/is-rIS/user/messages-and-channels.md index 3cb2cccf85..3eca099119 100644 --- a/docs/is-rIS/user/messages-and-channels.md +++ b/docs/is-rIS/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/is-rIS/user/mqtt.md b/docs/is-rIS/user/mqtt.md index 0e6ba39685..4bede80a57 100644 --- a/docs/is-rIS/user/mqtt.md +++ b/docs/is-rIS/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/is-rIS/user/node-metrics.md b/docs/is-rIS/user/node-metrics.md index 63c15f5dd5..ad6766fabd 100644 --- a/docs/is-rIS/user/node-metrics.md +++ b/docs/is-rIS/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/is-rIS/user/nodes.md b/docs/is-rIS/user/nodes.md index b4be385d56..e08db5eeed 100644 --- a/docs/is-rIS/user/nodes.md +++ b/docs/is-rIS/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Lýsing | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Lýsing | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Lýsing | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Last heard | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distance | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/is-rIS/user/notifications.md b/docs/is-rIS/user/notifications.md new file mode 100644 index 0000000000..e158ce9973 --- /dev/null +++ b/docs/is-rIS/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Tilkynningar um þjónustu | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/is-rIS/user/onboarding.md b/docs/is-rIS/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/is-rIS/user/onboarding.md +++ b/docs/is-rIS/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/is-rIS/user/settings-module-admin.md b/docs/is-rIS/user/settings-module-admin.md index 4bf88f9a61..85351ca991 100644 --- a/docs/is-rIS/user/settings-module-admin.md +++ b/docs/is-rIS/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Stillingar aukaeininga Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Lýsing | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Lýsing | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Lýsing | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Lýsing | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Lýsing | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Lýsing | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Lýsing | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Lýsing | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Lýsing | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Lýsing | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Lýsing | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Lýsing | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Lýsing | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Lýsing | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Lýsing | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Lýsing | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Lýsing | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Lýsing | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Endurræsa | Restarts the radio | -| Slökkva | Powers the radio down | -| Grunnstilla | Returns every setting to its factory default | -| Endurræsa NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Endurræsa | Restarts the node | +| Slökkva | Powers the node down | +| Grunnstilla | Returns every setting to its factory default | +| Endurræsa NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Um smáforrit @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/is-rIS/user/settings-radio-user.md b/docs/is-rIS/user/settings-radio-user.md index 5bd597ffe2..87fa878269 100644 --- a/docs/is-rIS/user/settings-radio-user.md +++ b/docs/is-rIS/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Lýsing | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Lýsing | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Lýsing | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Svæði | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Lýsing | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Svæði | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Lýsing | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Lýsing | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Lýsing | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Lýsing | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Lýsing | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Lýsing | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Lýsing | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/is-rIS/user/signal-meter.md b/docs/is-rIS/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/is-rIS/user/signal-meter.md +++ b/docs/is-rIS/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/is-rIS/user/tak.md b/docs/is-rIS/user/tak.md index f3c0aef2bf..fe90bfe017 100644 --- a/docs/is-rIS/user/tak.md +++ b/docs/is-rIS/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/is-rIS/user/telemetry-and-sensors.md b/docs/is-rIS/user/telemetry-and-sensors.md index 782f131029..edce2acb84 100644 --- a/docs/is-rIS/user/telemetry-and-sensors.md +++ b/docs/is-rIS/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distance | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distance | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/is-rIS/user/translate.md b/docs/is-rIS/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/is-rIS/user/translate.md +++ b/docs/is-rIS/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/is-rIS/user/units-and-locale.md b/docs/is-rIS/user/units-and-locale.md index a09eee3f53..8ce5008e4a 100644 --- a/docs/is-rIS/user/units-and-locale.md +++ b/docs/is-rIS/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/is-rIS/user/widget.md b/docs/is-rIS/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/is-rIS/user/widget.md +++ b/docs/is-rIS/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/it-rIT/user/app-functions.md b/docs/it-rIT/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/it-rIT/user/app-functions.md +++ b/docs/it-rIT/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/it-rIT/user/connections.md b/docs/it-rIT/user/connections.md index 33f25c751d..d86d01e9a3 100644 --- a/docs/it-rIT/user/connections.md +++ b/docs/it-rIT/user/connections.md @@ -1,8 +1,7 @@ --- title: Connessioni -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Rete | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/it-rIT/user/debug-logs.md b/docs/it-rIT/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/it-rIT/user/debug-logs.md +++ b/docs/it-rIT/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/it-rIT/user/desktop.md b/docs/it-rIT/user/desktop.md index 2abaa5f6bd..762e3bc915 100644 --- a/docs/it-rIT/user/desktop.md +++ b/docs/it-rIT/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/it-rIT/user/discovery.md b/docs/it-rIT/user/discovery.md index 2940c1e849..1042f94676 100644 --- a/docs/it-rIT/user/discovery.md +++ b/docs/it-rIT/user/discovery.md @@ -1,8 +1,7 @@ --- title: Discovery della mesh locale -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descrizione | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descrizione | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informazioni Vicinato @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/it-rIT/user/firmware.md b/docs/it-rIT/user/firmware.md index 5091504f8f..6312fa0e5d 100644 --- a/docs/it-rIT/user/firmware.md +++ b/docs/it-rIT/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/it-rIT/user/help-and-docs.md b/docs/it-rIT/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/it-rIT/user/help-and-docs.md +++ b/docs/it-rIT/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/it-rIT/user/map-and-waypoints.md b/docs/it-rIT/user/map-and-waypoints.md index b221eb35d5..28503c66b4 100644 --- a/docs/it-rIT/user/map-and-waypoints.md +++ b/docs/it-rIT/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Livelli della mappa -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/it-rIT/user/messages-and-channels.md b/docs/it-rIT/user/messages-and-channels.md index f2c8d0d200..6dad4911dd 100644 --- a/docs/it-rIT/user/messages-and-channels.md +++ b/docs/it-rIT/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/it-rIT/user/mqtt.md b/docs/it-rIT/user/mqtt.md index 3145e19feb..c5011869df 100644 --- a/docs/it-rIT/user/mqtt.md +++ b/docs/it-rIT/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/it-rIT/user/node-metrics.md b/docs/it-rIT/user/node-metrics.md index d5d6668c56..e0aea97171 100644 --- a/docs/it-rIT/user/node-metrics.md +++ b/docs/it-rIT/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/it-rIT/user/nodes.md b/docs/it-rIT/user/nodes.md index fa03c91053..2d349a63f8 100644 --- a/docs/it-rIT/user/nodes.md +++ b/docs/it-rIT/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodi -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensore | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Oggetti Smarriti | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Oggetti Smarriti | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtro | Descrizione | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrizione | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descrizione | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Ricevuto più di recente | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distanza | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/it-rIT/user/notifications.md b/docs/it-rIT/user/notifications.md new file mode 100644 index 0000000000..5dc585965e --- /dev/null +++ b/docs/it-rIT/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messaggi | Notifiche di messaggi diretti | A message sent directly to you | The conversation | +| Messaggi | Notifiche di messaggi broadcast | A message on one of your channels | The channel | +| Messaggi | Notifiche Waypoint | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messaggi | Notifiche di allarme | A critical alert from a node | The conversation | +| Mesh | Notifiche di nuovi nodi | A node heard for the first time | The node's details | +| Mesh | Notifiche di invito mesh | An invitation to join a nearby mesh | Discovery della mesh locale | +| Mesh | Notifiche batteria scarica (nodi preferiti) | A favorite node's battery running low | The node's details | +| Dispositivo | Notifiche di servizio | The connection to your node while the app runs in the background | The app | +| Dispositivo | Notifica di batteria scarica | Your node's battery running low | The node's details | +| Dispositivo | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Dispositivo | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/it-rIT/user/onboarding.md b/docs/it-rIT/user/onboarding.md index 423f97a7ff..226821bb7b 100644 --- a/docs/it-rIT/user/onboarding.md +++ b/docs/it-rIT/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Come iniziare -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/it-rIT/user/settings-module-admin.md b/docs/it-rIT/user/settings-module-admin.md index 32ff37ab30..9099862bd4 100644 --- a/docs/it-rIT/user/settings-module-admin.md +++ b/docs/it-rIT/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Configurazione modulo +## Impostazioni moduli Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Impostazione | Descrizione | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT abilitato | Toggle MQTT bridge | -| Indirizzo | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Crittografia abilitata | Encrypt MQTT payloads | -| Output JSON abilitato | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS abilitato | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client attivato | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| Proxy MQTT su questo telefono | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Segnalazione su mappa | Publish position to the public map — see the Map reporting group that follows | +| Impostazione | Descrizione | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT abilitato | Toggle MQTT bridge | +| Indirizzo | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Crittografia abilitata | Encrypt MQTT payloads | +| Output JSON abilitato | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS abilitato | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client attivato | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Segnalazione su mappa | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Impostazione | Descrizione | -| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sono d’accordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervallo di segnalazione su mappa (secondi) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Impostazione | Descrizione | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sono d’accordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Sovrascrivi porta seriale della console | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Impostazione | Descrizione | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notifica esterna attivata | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED Output (GPIO) | Pin the LED is wired to | -| Output per LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibrazione (GPIO) | Pin the vibration motor is wired to | -| Usa buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Usa I2S come buzzer | Send the alert through an I2S audio output instead | -| Durata output (millisecondi) | How long a single alert lasts | -| Timeout chiusura popup (secondi) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Suoneria | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Impostazione | Descrizione | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Notifica esterna attivata | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED Output (GPIO) | Pin the LED is wired to | +| Output per LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibrazione (GPIO) | Pin the vibration motor is wired to | +| Usa buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Usa I2S come buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Suoneria | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Impostazione | Descrizione | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| Test distanza massima abilitato | Activate range testing | -| Intervallo messaggio mittente (secondi) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Salva .CSV nella memoria (solo ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Impostazione | Descrizione | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| Test distanza massima abilitato | Activate range testing | +| Intervallo Del Mittente | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Salva .CSV nella memoria (solo ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Impostazione | Descrizione | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Invia Telemetria Dispositivo | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Intervallo aggiornamento metriche dispositivo | How often to report battery, uptime and channel utilization | -| Modulo metriche ambientali abilitato | Report the attached environment sensors | -| Intervallo aggiornamento metriche ambientali | How often to report them | -| Metriche ambientali visualizzate su schermo | Also show these readings on the device's own display | -| Usa i gradi Fahrenheit nelle metriche ambientali | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Modulo metriche della qualità dell'aria abilitato | Report particulate and CO₂ sensor data | -| Intervallo aggiornamento metriche qualità aria | How often to report them | -| Modulo metriche di alimentazione abilitato | Report the per-channel voltage and current readings | -| Intervallo aggiornamento metriche alimentazione | How often to report them | -| Metriche di alimentazione visualizzate su schermo | Also show power readings on the device's display | +| Impostazione | Descrizione | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Invia Telemetria Dispositivo | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Intervallo aggiornamento metriche dispositivo | How often to report battery, uptime and channel utilization | +| Modulo metriche ambientali abilitato | Report the attached environment sensors | +| Intervallo aggiornamento metriche ambientali | How often to report them | +| Metriche ambientali visualizzate su schermo | Also show these readings on the device's own display | +| Usa i gradi Fahrenheit nelle metriche ambientali | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Modulo metriche della qualità dell'aria abilitato | Report particulate and CO₂ sensor data | +| Intervallo aggiornamento metriche qualità aria | How often to report them | +| Modulo metriche di alimentazione abilitato | Report the per-channel voltage and current readings | +| Intervallo aggiornamento metriche alimentazione | How often to report them | +| Metriche di alimentazione visualizzate su schermo | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Impostazione | Descrizione | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Input Su/Giu/Selezione abilitato | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Impostazione | Descrizione | | ----------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Consenti accesso a pin non definiti | Allow access to any GPIO pin (security risk) | | Pin disponibili | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Impostazione | Descrizione | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Info Nodi Vicini abilitato | Activate neighbor broadcasting | -| Intervallo di aggiornamento (secondi) | How often to broadcast neighbor list | -| Trasmettere su LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Impostazione | Descrizione | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Info Nodi Vicini abilitato | Activate neighbor broadcasting | +| Intervallo Interrogazione GPS | How often to broadcast neighbor list | +| Trasmettere su LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Attuale | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Impostazione | Descrizione | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| Sensore Rilevamento attivo | Activate detection sensor | -| Pin GPIO da monitorare | GPIO pin connected to sensor | -| Tipo di trigger di rilevamento | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Usa modalità INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Trasmissione minima (secondi) | Minimum time between alert broadcasts | -| Trasmissione stato (secondi) | Periodic state broadcast interval | -| Invia campanella con messaggio di avviso | Include bell character in alerts | -| Nome semplificato | Custom name for this sensor | +| Impostazione | Descrizione | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Sensore Rilevamento attivo | Activate detection sensor | +| Pin GPIO da monitorare | GPIO pin connected to sensor | +| Tipo di trigger di rilevamento | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Usa modalità INPUT_PULLUP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| Intervallo Di Trasmissione | Periodic state broadcast interval | +| Invia campanella con messaggio di avviso | Include bell character in alerts | +| Nome semplificato | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Impostazione | Descrizione | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter abilitato | Activate people counting | -| Intervallo di aggiornamento (secondi) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Impostazione | Descrizione | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter abilitato | Activate people counting | +| Intervallo Interrogazione GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Imposta l'ora | Sends your phone's clock to the radio | -| Riavvia | Restarts the radio | -| Spegni | Powers the radio down | -| Ripristina impostazioni di fabbrica | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| Imposta l'ora | Sends your phone's clock to the node | +| Riavvia | Restarts the node | +| Spegni | Powers the node down | +| Ripristina impostazioni di fabbrica | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Ripristino -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avanzate **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Azzera il database dei nodi -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Informazioni @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/it-rIT/user/settings-radio-user.md b/docs/it-rIT/user/settings-radio-user.md index 81dae9a4fe..cc95ee066c 100644 --- a/docs/it-rIT/user/settings-radio-user.md +++ b/docs/it-rIT/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - impostazioni - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Impostazione | Descrizione | -| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nome Lungo | Your display name (up to 39 characters) | -| Nome Breve | 4-character abbreviated name | -| Messaggio di Stato | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Non messaggiabile | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Radioamatore con licenza (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Impostazione | Descrizione | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nome Lungo | Your display name (up to 39 characters) | +| Nome Breve | 4-character abbreviated name | +| Messaggio di Stato | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Non messaggiabile | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Modalità Ritrasmissione | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Intervallo Di Trasmissione Info Nodo | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Doppio tocco come pressione pulsante | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Abilitato | | Battito Cuore Led | Blink the status LED periodically | Abilitato | | Fuso Orario | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Impostazione | Descrizione | Predefinito | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regione | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Preset | Speed/range tradeoff | LongFast | -| Numero di Hop | Maximum retransmit hops | 3 | -| Potenza di Trasmissione | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Sovrascrivi Frequenza | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Usa Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Larghezza di banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Slot di Frequenza | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Trasmissione Abilitata | Turning this off makes the node receive-only | On | -| Ignora limite di Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Disattivato | -| Ignora MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| OK per MQTT | Allow your packets to be forwarded to MQTT by gateways | Disattivato | -| Migliora guadagno in Ricezione | Extra receive gain on SX126x radios; costs a little current | Disattivato | -| Ventola PA disabilitata | Turn off the power-amplifier fan on hardware that has one | Disattivato | +| Impostazione | Descrizione | Predefinito | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regione | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Preset | Speed/range tradeoff | LongFast | +| Numero di Hop | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Sovrascrivi Frequenza | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Usa Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Larghezza di banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Slot di Frequenza | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Trasmissione Abilitata | Turning this off makes the node receive-only | On | +| Ignora limite di Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Disattivato | +| Ignora MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| OK per MQTT | Allow your packets to be forwarded to MQTT by gateways | Disattivato | +| Migliora guadagno in Ricezione | Extra receive gain on SX126x radios; costs a little current | Disattivato | +| Ventola PA disabilitata | Turn off the power-amplifier fan on hardware that has one | Disattivato | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0,18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configurazione Schermo -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Impostazione | Descrizione | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Tieni lo schermo acceso per | How long the display stays lit before sleeping | -| Durata di ogni schermata | How often the radio cycles between screens on its own | -| Modalità schermo | Screen layout/density used by the firmware | -| Unità di misura visualizzata | Metric or Imperial on the radio's screen | -| Usa formato orologio 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Intestazione in grassetto | Draw the screen's heading text in bold | -| Capovolgi schermo | Rotate the display 180° for an inverted mounting | -| Tipo OLED | Auto, SSD1306, SH1106, SH1107 | -| Accendi lo schermo al tocco o al movimento | Light the screen when the radio is tapped or moved | -| Orientamento bussola | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Punta sempre a nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Impostazione | Descrizione | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Tieni lo schermo acceso per | How long the display stays lit before sleeping | +| Durata di ogni schermata | How often the node cycles between screens on its own | +| Modalità schermo | Screen layout/density used by the firmware | +| Unità di misura visualizzata | Metric or Imperial on the node's screen | +| Usa formato orologio 12h | Show the node's clock as 12-hour rather than 24-hour | +| Intestazione in grassetto | Draw the screen's heading text in bold | +| Capovolgi schermo | Rotate the display 180° for an inverted mounting | +| Tipo OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Accendi lo schermo al tocco o al movimento | Light the screen when the node is tapped or moved | +| Orientamento bussola | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Punta sempre a nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configurazione Posizione On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Impostazione | Descrizione | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Modalità GPS (Hardware Fisico) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Intervallo Interrogazione GPS | How often the radio asks its GPS for a fix | +| Intervallo Interrogazione GPS | How often the node asks its GPS for a fix | | Intervallo Di Trasmissione | How often the position is shared with the mesh | | Posizione Smart | Broadcast based on movement rather than purely on the clock | | Intervallo Intelligente | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Impostazione | Descrizione | | --------------------------------------------------------- | --------------------------------------------------------------- | -| Abilita modalità risparmio energetico | Let the radio sleep aggressively between activity | +| Abilita modalità risparmio energetico | Let the node sleep aggressively between activity | | Spegnimento in mancanza di alimentazione | Power the device down after external power disappears | | Durata super deep sleep | How long the deepest sleep state lasts | -| Tempo minimo di risveglio | The shortest time the radio stays awake once woken | +| Tempo minimo di risveglio | The shortest time the node stays awake once woken | | Durata attesa Bluetooth | How long to wait for a phone to connect before sleeping | | Sovrascrivi moltiplicatore ADC | Turn on a manual correction for battery-voltage readings | | Sovrascrivi rapporto moltiplicatore ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configurazione Della Rete -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Impostazione | Descrizione | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Password di rete | | Ethernet abilitato | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configurazione Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Impostazione | Descrizione | | -------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Impostazione | Descrizione | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chiave Pubblica | Your node's public key (read-only) | | Chiave Amministratore | Keys permitted to administer this node remotely — up to three | -| Chiave Privata | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Chiave Privata | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Rigenera Chiavi Private | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Console seriale | Serial console over the Stream API | -| Debug log API abilitato | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Modalità Gestita | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API abilitato | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Modalità Gestita | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup chiavi | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/it-rIT/user/signal-meter.md b/docs/it-rIT/user/signal-meter.md index b7b8a20c72..06d55ceea2 100644 --- a/docs/it-rIT/user/signal-meter.md +++ b/docs/it-rIT/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/it-rIT/user/tak.md b/docs/it-rIT/user/tak.md index f1fbcf4688..d89f092721 100644 --- a/docs/it-rIT/user/tak.md +++ b/docs/it-rIT/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/it-rIT/user/telemetry-and-sensors.md b/docs/it-rIT/user/telemetry-and-sensors.md index 8d54127989..e431960260 100644 --- a/docs/it-rIT/user/telemetry-and-sensors.md +++ b/docs/it-rIT/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensore | Metrico | Note | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensore | Metrico | Note | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensore | Metrico | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metrico | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiazione | µR/h | Card and chart | -| Peso | kg or lb | Card only — load cells, such as a beehive scale | -| Distanza | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metrico | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiazione | µR/h | Card and chart | +| Peso | kg or lb | Card only — load cells, such as a beehive scale | +| Distanza | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Metriche Alimentazione diff --git a/docs/it-rIT/user/translate.md b/docs/it-rIT/user/translate.md index 0404022919..d3b873a7f8 100644 --- a/docs/it-rIT/user/translate.md +++ b/docs/it-rIT/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/it-rIT/user/units-and-locale.md b/docs/it-rIT/user/units-and-locale.md index 4840c00725..72d68a30e8 100644 --- a/docs/it-rIT/user/units-and-locale.md +++ b/docs/it-rIT/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/it-rIT/user/widget.md b/docs/it-rIT/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/it-rIT/user/widget.md +++ b/docs/it-rIT/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/iw-rIL/user/app-functions.md b/docs/iw-rIL/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/iw-rIL/user/app-functions.md +++ b/docs/iw-rIL/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/iw-rIL/user/connections.md b/docs/iw-rIL/user/connections.md index b0f3fb1c12..46758122ad 100644 --- a/docs/iw-rIL/user/connections.md +++ b/docs/iw-rIL/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/iw-rIL/user/debug-logs.md b/docs/iw-rIL/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/iw-rIL/user/debug-logs.md +++ b/docs/iw-rIL/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/iw-rIL/user/desktop.md b/docs/iw-rIL/user/desktop.md index 4c4af4607f..dfac5720bb 100644 --- a/docs/iw-rIL/user/desktop.md +++ b/docs/iw-rIL/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/iw-rIL/user/discovery.md b/docs/iw-rIL/user/discovery.md index 4e4cb8af8b..b9fd6ec0e6 100644 --- a/docs/iw-rIL/user/discovery.md +++ b/docs/iw-rIL/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | תיאור | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | תיאור | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/iw-rIL/user/firmware.md b/docs/iw-rIL/user/firmware.md index 416f1854ee..9f6d8aebee 100644 --- a/docs/iw-rIL/user/firmware.md +++ b/docs/iw-rIL/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/iw-rIL/user/help-and-docs.md b/docs/iw-rIL/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/iw-rIL/user/help-and-docs.md +++ b/docs/iw-rIL/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/iw-rIL/user/map-and-waypoints.md b/docs/iw-rIL/user/map-and-waypoints.md index 4c735ebc51..730cc76b36 100644 --- a/docs/iw-rIL/user/map-and-waypoints.md +++ b/docs/iw-rIL/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/iw-rIL/user/messages-and-channels.md b/docs/iw-rIL/user/messages-and-channels.md index c68ba11050..50a35af5a6 100644 --- a/docs/iw-rIL/user/messages-and-channels.md +++ b/docs/iw-rIL/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/iw-rIL/user/mqtt.md b/docs/iw-rIL/user/mqtt.md index 78816530eb..ed9076cb3c 100644 --- a/docs/iw-rIL/user/mqtt.md +++ b/docs/iw-rIL/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/iw-rIL/user/node-metrics.md b/docs/iw-rIL/user/node-metrics.md index 9bd2925f69..32d0631b5c 100644 --- a/docs/iw-rIL/user/node-metrics.md +++ b/docs/iw-rIL/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/iw-rIL/user/nodes.md b/docs/iw-rIL/user/nodes.md index dde88a46b0..9b06b45b78 100644 --- a/docs/iw-rIL/user/nodes.md +++ b/docs/iw-rIL/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| פילטר | תיאור | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| פילטר | תיאור | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | תיאור | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Last heard | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | מרחק | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/iw-rIL/user/notifications.md b/docs/iw-rIL/user/notifications.md new file mode 100644 index 0000000000..2b0c2bba5c --- /dev/null +++ b/docs/iw-rIL/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| הודעות | Direct message notifications | A message sent directly to you | The conversation | +| הודעות | Broadcast message notifications | A message on one of your channels | The channel | +| הודעות | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| הודעות | התראות | A critical alert from a node | The conversation | +| Mesh | New node notifications | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | התראות שירות | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/iw-rIL/user/onboarding.md b/docs/iw-rIL/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/iw-rIL/user/onboarding.md +++ b/docs/iw-rIL/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/iw-rIL/user/settings-module-admin.md b/docs/iw-rIL/user/settings-module-admin.md index c0f5f7d062..421572a9b2 100644 --- a/docs/iw-rIL/user/settings-module-admin.md +++ b/docs/iw-rIL/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## הגדרות מודולות Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | תיאור | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | תיאור | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | תיאור | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | תיאור | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | תיאור | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | תיאור | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | תיאור | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | תיאור | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | תיאור | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | תיאור | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | תיאור | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | תיאור | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | תיאור | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | תיאור | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | תיאור | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | תיאור | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | תיאור | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | תיאור | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| אתחול מחדש | Restarts the radio | -| כיבוי | Powers the radio down | -| איפוס להגדרות היצרן | Returns every setting to its factory default | -| איפוס NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| אתחול מחדש | Restarts the node | +| כיבוי | Powers the node down | +| איפוס להגדרות היצרן | Returns every setting to its factory default | +| איפוס NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### אודות @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/iw-rIL/user/settings-radio-user.md b/docs/iw-rIL/user/settings-radio-user.md index c650b57100..eecff0350f 100644 --- a/docs/iw-rIL/user/settings-radio-user.md +++ b/docs/iw-rIL/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - הגדרות - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | תיאור | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | תיאור | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | תיאור | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| אזור | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | תיאור | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| אזור | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | תיאור | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | תיאור | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | תיאור | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | תיאור | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | תיאור | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | תיאור | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | תיאור | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/iw-rIL/user/signal-meter.md b/docs/iw-rIL/user/signal-meter.md index 12d6aaade0..d70b892e2b 100644 --- a/docs/iw-rIL/user/signal-meter.md +++ b/docs/iw-rIL/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/iw-rIL/user/tak.md b/docs/iw-rIL/user/tak.md index 62f5ec7a71..955d0cd291 100644 --- a/docs/iw-rIL/user/tak.md +++ b/docs/iw-rIL/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/iw-rIL/user/telemetry-and-sensors.md b/docs/iw-rIL/user/telemetry-and-sensors.md index 47d5a159cc..efd2747436 100644 --- a/docs/iw-rIL/user/telemetry-and-sensors.md +++ b/docs/iw-rIL/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| מרחק | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| מרחק | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/iw-rIL/user/translate.md b/docs/iw-rIL/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/iw-rIL/user/translate.md +++ b/docs/iw-rIL/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/iw-rIL/user/units-and-locale.md b/docs/iw-rIL/user/units-and-locale.md index a09eee3f53..8ce5008e4a 100644 --- a/docs/iw-rIL/user/units-and-locale.md +++ b/docs/iw-rIL/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/iw-rIL/user/widget.md b/docs/iw-rIL/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/iw-rIL/user/widget.md +++ b/docs/iw-rIL/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ja-rJP/user/app-functions.md b/docs/ja-rJP/user/app-functions.md index 8cf1485894..167ac8a2a8 100644 --- a/docs/ja-rJP/user/app-functions.md +++ b/docs/ja-rJP/user/app-functions.md @@ -1,6 +1,5 @@ --- title: アプリ機能 -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: メッシュの機能を Android システムやオンデバイスの AI アシスタント(例:Gemini)に公開し、アプリを開かずにメッシュのワークフローを実行できるようにします。 @@ -13,7 +12,7 @@ aliases: # アプリ機能 -アプリ機能は、Android App Functions API を通じて、Meshtastic の機能を Android システムやオンデバイスの AI アシスタント(Gemini など)に公開します。 有効にすると、アシスタントがあなたに代わってメッシュのワークフロー(例:メッセージの送信やメッシュ状態の確認)を見つけて実行でき、アプリを開く必要がありません。 App Functions are available on **Google-flavor Android builds only**. +アプリ機能は、Android App Functions API を通じて、Meshtastic の機能を Android システムやオンデバイスの AI アシスタント(Gemini など)に公開します。有効にすると、アシスタントがあなたに代わってメッシュのワークフロー(例:メッセージの送信やメッシュ状態の確認)を見つけて実行でき、アプリを開く必要がありません。 App Functions are available on **Google-flavor Android builds only**. > ℹ️ **Note:** This is separate from the in-app **Chirpy** assistant. アプリ機能は_システム_の AI アシスタントがあなたのメッシュを操作できるようにするもので、Chirpy は Meshtastic アプリ内の対話型アシスタントです。 @@ -21,7 +20,7 @@ aliases: Control App Functions from **Settings → System AI**. この画面には次があります: -- 「**AI のアクセスを許可**」というラベルの**マスタートグル**。サブタイトルは _「システムの AI アシスタント(例:Gemini)がメッシュ機能を見つけて使えるようにする」_ です。 オフの場合、システムには機能が一切公開されません。 +- 「**AI のアクセスを許可**」というラベルの**マスタートグル**。サブタイトルは _「システムの AI アシスタント(例:Gemini)がメッシュ機能を見つけて使えるようにする」_ です。オフの場合、システムには機能が一切公開されません。 - **各機能ごとの個別トグル**。公開したい機能だけを公開できます。 > ⚠️ **Important:** App Functions ship switched on. On a Google-flavor build the master toggle and every individual function, **Send message** included, start enabled — so an assistant can read your mesh data and send messages to your mesh until you turn **Allow AI access** off. diff --git a/docs/ja-rJP/user/connections.md b/docs/ja-rJP/user/connections.md index 5cd88f87d9..1d86bbc3bf 100644 --- a/docs/ja-rJP/user/connections.md +++ b/docs/ja-rJP/user/connections.md @@ -1,8 +1,7 @@ --- title: コネクション -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: スマートフォンやデスクトップを、Bluetooth・USB・TCP/IP で Meshtastic 無線機に接続します。 aliases: - bluetooth @@ -39,12 +38,12 @@ You can change the pairing method, or turn Bluetooth on for a radio that ships w The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | 意味 | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | 意味 | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![接続中のステータス](../../assets/screenshots/connections_connecting.png) -デバイスが見つからない場合、アプリは操作手順とともに空の状態を表示します: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![デバイスが見つからない状態](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| ネットワーク | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Bluetooth のトラブルシューティング @@ -102,7 +105,7 @@ Some Meshtastic radios support Wi-Fi/Ethernet connectivity, allowing TCP-based c 1. 無線機がスマートフォン/デスクトップと同じローカルネットワーク上にあることを確認します。 2. On the **Connect** tab, select **Network** in the transport selector. 3. 無線機は次の 2 通りの方法で選べます: - - **ネットワークデバイスをスキャン**:これをオンにすると、ローカルネットワーク上で自身を告知している無線機(mDNS / `_meshtastic._tcp`)を自動的に探索します。 見つかったデバイスがリストに表示されるので、タップして接続します。 + - **ネットワークデバイスをスキャン**:これをオンにすると、ローカルネットワーク上で自身を告知している無線機(mDNS / `_meshtastic._tcp`)を自動的に探索します。見つかったデバイスがリストに表示されるので、タップして接続します。 - **デバイスを手動で追加…**:無線機の IP アドレス(またはホスト名)とポート(既定:`4403`)を入力します。 4. Previously-used network addresses are remembered under **Recent Network Devices** for quick reconnection (touch & hold to remove one). diff --git a/docs/ja-rJP/user/debug-logs.md b/docs/ja-rJP/user/debug-logs.md index 0f497ac79e..d359291005 100644 --- a/docs/ja-rJP/user/debug-logs.md +++ b/docs/ja-rJP/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: デバッグログ -parent: ユーザーガイド nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: アプリのデバッグログをアプリ内で表示・エクスポートし、バグの診断に役立つよう GitHub の issue にキャプチャを添付できます。adb は不要です。 aliases: - debug-logs @@ -23,8 +22,8 @@ If you're filing an issue, export your logs (see [Exporting](#exporting)) and at デバッグパネルには 2 つのタブがあります: -- **パケット**:無線機が送受信した、デコード済みのメッシュトラフィック(プロトコルレベルのメッセージ)。 メッシュやルーティングの動作を診断するのに役立ちます。 -- **アプリログ**:アプリ自身の診断ログ(Android の _logcat_)。アプリからの警告、エラー、スタックトレースを含みます。 通常、バグ報告に必要なのはこちらです。 +- **パケット**:無線機が送受信した、デコード済みのメッシュトラフィック(プロトコルレベルのメッセージ)。メッシュやルーティングの動作を診断するのに役立ちます。 +- **アプリログ**:アプリ自身の診断ログ(Android の _logcat_)。アプリからの警告、エラー、スタックトレースを含みます。通常、バグ報告に必要なのはこちらです。 各タブには独自の**エクスポート**ボタンがあり、それぞれ別のファイルを生成するため、関連する方を、または両方を取得できます。 @@ -33,7 +32,7 @@ If you're filing an issue, export your logs (see [Exporting](#exporting)) and at The **App logs** tab shows the most recent log lines from **this app only** — never other apps on your phone. - **検索**:検索ボックスに入力すると、一致する行に絞り込めます。 -- **レベルフィルター**:**V/D/I/W/E** のチップで、Verbose、Debug、Info、Warn、Error の行を切り替えます。 レベルをタップすると非表示になり、もう一度タップすると再表示されます。 Fatal の行は常に表示されます。 +- **レベルフィルター**:**V/D/I/W/E** のチップで、Verbose、Debug、Info、Warn、Error の行を切り替えます。レベルをタップすると非表示になり、もう一度タップすると再表示されます。 Fatal の行は常に表示されます。 - **更新**:更新アイコンで最新のログを読み直します。 エラーと警告の行は色付けされ、問題が目立つようになっています。 @@ -48,7 +47,7 @@ The **App logs** tab shows the most recent log lines from **this app only** — ## デスクトップ -デスクトップアプリにはシステムの logcat がないため、「**アプリログ**」タブは代わりに、アプリ自身がキャプチャしたログ出力を表示します。 検索、絞り込み、エクスポートは同じように機能します。 +デスクトップアプリにはシステムの logcat がないため、「**アプリログ**」タブは代わりに、アプリ自身がキャプチャしたログ出力を表示します。検索、絞り込み、エクスポートは同じように機能します。 Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## 関連トピック diff --git a/docs/ja-rJP/user/desktop.md b/docs/ja-rJP/user/desktop.md index ecc4fb087a..d13770dae1 100644 --- a/docs/ja-rJP/user/desktop.md +++ b/docs/ja-rJP/user/desktop.md @@ -1,6 +1,5 @@ --- title: デスクトップアプリ -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Linux、macOS、Windows で Meshtastic デスクトップアプリをインストールして使う方法。接続、機能の対応状況、キーボードショートカットを説明します。 @@ -53,7 +52,7 @@ Connect your radio via USB. The app detects the serial port automatically; if it Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/JuulLabs/kable) library: -1. システムに Bluetooth アダプターがあることを確認します。 アプリが近くの Meshtastic 無線機を自動的にスキャンします。 +1. システムに Bluetooth アダプターがあることを確認します。アプリが近くの Meshtastic 無線機を自動的にスキャンします。 2. Select your radio from the Connect screen. ## 機能の対応状況 @@ -75,7 +74,7 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ \*Chirpy AI には、対応ハードウェアを備えた Google 版ビルドで Android 14 以降が必要です。 -†アプリ機能は、Google 版ビルドで、アプリの操作を Android のシステム AI に公開します。 [アプリ機能](app-functions) を参照してください。 +†アプリ機能は、Google 版ビルドで、アプリの操作を Android のシステム AI に公開します。[アプリ機能](app-functions) を参照してください。 ## UI の違い diff --git a/docs/ja-rJP/user/discovery.md b/docs/ja-rJP/user/discovery.md index b8091b7817..15b1c29901 100644 --- a/docs/ja-rJP/user/discovery.md +++ b/docs/ja-rJP/user/discovery.md @@ -1,8 +1,7 @@ --- title: ローカルメッシュ探索 -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: メッシュネットワークを探索します。ローカルメッシュ探索スキャナー、ルート追跡の経路、隣接ノードのマップ、ノード探索ツールを説明します。 aliases: - discovery @@ -20,53 +19,53 @@ aliases: アプリは、互いを補完する 2 つの方法を提供します: -- **ローカルメッシュ探索(スキャナー)**:接続中の無線機をさまざまな LoRa プリセットで順に切り替え、それぞれで受信し、あなたの場所でどのプリセットが最も性能が良いかをランク付けする自動モードです。 +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **手動での探索**:ルート追跡、隣接ノード情報、ノードリスト。特定の経路やトポロジーを調べるために、いつでも使えます。 ## ローカルメッシュ探索(スキャナー) -ローカルメッシュ探索は、あなたの場所に最適な LoRa モデムプリセットを見つけ、各プリセットでどのノードがアクティブかを確認できる、専用のスキャンモードです。 It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +ローカルメッシュ探索は、あなたの場所に最適な LoRa モデムプリセットを見つけ、各プリセットでどのノードがアクティブかを確認できる、専用のスキャンモードです。 It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### スキャンを設定する +### Setting up a scan 開始する前に、次のコントロールを設定します: -| コントロール | 説明 | -| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **LoRa プリセットの選択** | スキャンするプリセットを 1 つ以上選択します。 探索は、選択した各プリセットに順番に滞在します。 | -| **滞在時間** | 各プリセットで受信する時間。 1、5、15、30、45、60、90、120、180 分から選択します。 滞在時間を長くすると、より多くのパケットを収集してより明確な状況が分かりますが、時間もかかります。 | -| **画面をスリープさせない** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| コントロール | 説明 | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa プリセットの選択** | スキャンするプリセットを 1 つ以上選択します。探索は、選択した各プリセットに順番に滞在します。 | +| **滞在時間** | 各プリセットで受信する時間。 1、5、15、30、45、60、90、120、180 分から選択します。滞在時間を長くすると、より多くのパケットを収集してより明確な状況が分かりますが、時間もかかります。 | +| **画面をスリープさせない** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. 無効になる主な理由: -- The radio is **not connected**. +- The node is **not connected**. - スキャンする**プリセットが選択されていない**。 - 選択したプリセットが、ハードウェアが対応していない **2.4 GHz** を使用している。 -### リアルタイムの進行状況 +### Live progress スキャンの実行中、探索は現在の段階を表示します: -| 段階 | 実行中の内容 | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | 現在の構成を保存し、スキャンの準備をしています。 | -| **\ に切り替え中** | テストする次のプリセットに無線機を切り替えています。 | -| **Reconnecting on \** | プリセット変更後に接続を再確立しています。 | -| **Dwelling on \** | 現在のプリセットで受信してパケットを収集しており、次のステップまでのカウントダウンが表示されます。 | -| **Analyzing results** | 収集したパケットを処理し、プリセットをランク付けしています。 | -| **Restoring home preset** | 元の LoRa 構成に戻しています。 | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| 段階 | 実行中の内容 | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | 現在の構成を保存し、スキャンの準備をしています。 | +| **\ に切り替え中** | Switching the node to the next preset to test. | +| **Reconnecting on \** | プリセット変更後に接続を再確立しています。 | +| **Dwelling on \** | 現在のプリセットで受信してパケットを収集しており、次のステップまでのカウントダウンが表示されます。 | +| **Analyzing results** | 収集したパケットを処理し、プリセットをランク付けしています。 | +| **Restoring home preset** | 元の LoRa 構成に戻しています。 | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![現在のプリセットの残り時間を示す滞在のカウントダウン](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### 結果の見方 +### Reading the results スキャンが完了すると、探索はテストした各プリセットのプリセット別の結果カードと、全体の概要を表示します。 @@ -94,41 +93,41 @@ If a scan is interrupted — the app is closed, or the radio goes away — the a メッシュビーコンを使うと、ノードが他のノードを自分のメッシュに招待できます。 A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **ビーコンを受信**:他のノードがブロードキャストする招待を受け取ります。 -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. -受信した招待は、探索画面に「**メッシュへの招待**」カードとして表示されます。 各カードには、送信者のメッセージと、提示されたチャンネル・リージョン・プリセット・信号品質が表示され、次の操作ができます: +受信した招待は、探索画面に「**メッシュへの招待**」カードとして表示されます。各カードには、送信者のメッセージと、提示されたチャンネル・リージョン・プリセット・信号品質が表示され、次の操作ができます: -- **参加**:提示されたチャンネルとプリセットに切り替えます(無線機を再調整して再起動します)。 提示内容が現在の周波数スロットと一致する場合は、再起動なしで追加できる「**チャンネルを追加**」の操作が表示されます。 +- **Join** — switch to the offered channel and preset (retunes the node and reboots). 提示内容が現在の周波数スロットと一致する場合は、再起動なしで追加できる「**チャンネルを追加**」の操作が表示されます。 - **探索**:提示されたプリセットで探索スキャンを開始し、参加する前にそのメッシュを調べられます(ビーコンがプリセットを提示している場合のみ表示)。 - **閉じる**:招待を無視します。 ビーコンが告知したチャンネルは、スキャン設定にも「**ビーコンのチャンネル**」として表示されます。選択すると、スキャン対象に含められます。 -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## 手動での探索 +## Manual exploration The following tools are available at any time from the node list and node detail screens. 完全なスキャンと併用して、またはその代わりに、特定の経路を調べてトポロジーの全体像を組み立てるのに使えます。 ### ルート追跡 -ルート追跡は、メッセージが自分のノードから、メッシュ上の他の任意のノードへ通る正確な経路を明らかにします。 接続の問題をデバッグするのに、最も役立つツールです。 +ルート追跡は、メッセージが自分のノードから、メッシュ上の他の任意のノードへ通る正確な経路を明らかにします。接続の問題をデバッグするのに、最も役立つツールです。 #### ルート追跡を実行する 1. 「**ノード**」に移動し、追跡したいノードをタップします。 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### 結果の見方 +#### Reading the results ルート追跡の結果は次のように表示されます: @@ -142,68 +141,68 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| 確認すべき点 | 意味 | -| ------------------------------------------------------------------ | ------------------------------------ | -| すべてのホップが良い SNR(−7 dB 以上、緑)を示す | 健全な経路。メッセージが確実に流れます | -| One hop shows a poor SNR (below −15 dB, orange) | 弱いリンク。この中継区間は脆弱です | -| ホップ数が多い(4 以上) | 長い経路。短くするためにノードの再配置を検討してください | -| 再試行で経路が変わる | メッシュが適応しています。複数の経路が存在します(これは良いことです!) | +| 確認すべき点 | 意味 | +| ----------------------------------------------------------- | ------------------------------------ | +| All hops show Good SNR (green) | 健全な経路。メッセージが確実に流れます | +| One hop shows a poor SNR (orange or red) | 弱いリンク。この中継区間は脆弱です | +| ホップ数が多い(4 以上) | 長い経路。短くするためにノードの再配置を検討してください | +| 再試行で経路が変わる | メッシュが適応しています。複数の経路が存在します(これは良いことです!) | -> 💡 **ヒント:** ルート追跡を数分間かけて何度か実行してください。 経路が変わる場合、メッシュには冗長な経路があり、よくつながったネットワークの兆候です。 +> 💡 **ヒント:** ルート追跡を数分間かけて何度か実行してください。経路が変わる場合、メッシュには冗長な経路があり、よくつながったネットワークの兆候です。 #### ルート追跡によるトラブルシューティング - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. 両方のノードが、同じ暗号化鍵を持つチャンネルを少なくとも 1 つ共有しているか確認してください。 - **ルート追跡がタイムアウトする**:経路が長すぎる(ホップ数上限を超える)か、中継ノードが混雑している可能性があります。 Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **非対称な経路**:A→B のルート追跡は、B→A とは異なる経路を通ることがあります。 これは正常です。電波の伝搬は常に対称とは限りません。 +- **非対称な経路**:A→B のルート追跡は、B→A とは異なる経路を通ることがあります。 This is normal — radio propagation isn't always symmetric. ### 隣接ノード情報 -隣接ノード情報モジュールを使うと、各ノードが**直接受信できる**(シングルホップの)ノードのリストをブロードキャストできます。 複数のノードが隣接ノードのリストを共有すると、メッシュ全体のトポロジーマップを組み立てられます。 +隣接ノード情報モジュールを使うと、各ノードが**直接受信できる**(シングルホップの)ノードのリストをブロードキャストできます。複数のノードが隣接ノードのリストを共有すると、メッシュ全体のトポロジーマップを組み立てられます。 #### 隣接ノード情報を有効にする 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. モジュールを有効にします。 -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. 隣接ノード情報を有効にしている他のノードも、同じことを行います。 -#### 隣接ノードのデータを表示する +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - 各隣接ノードの項目には、直接受信したノードとその信号品質が表示されます。 - 複数のノードの隣接ノードデータを組み合わせて、メッシュ全体のトポロジーを把握します。 -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### 探索ツールとしてのノードリスト +### Node list as a discovery tool ノードリスト自体も、絞り込みと並べ替えの機能を効果的に使えば、強力な探索ツールになります。 -#### 新しいノードを見つける +#### Finding new nodes - 「**最後の通信**」で並べ替えると、最近アクティブだったノードが先頭に表示されます。 -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### 接続性を評価する +#### Assessing connectivity - 「**ホップ数**」で並べ替えると、直接到達できるノード(0 ホップ)と中継されるノードが分かります。 - 「**距離**」で並べ替えると、近くのノードを見つけて、到達できるか確認できます。 -- 「**MQTT を除外**」を使うと、(インターネットブリッジ経由ではなく)無線で到達できるノードに絞り込めます。 +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### インフラの監査 +#### Infrastructure audit - 「**インフラを除外**」を無効にすると、ルーター、ルーター・レイト、クライアント・ベースのノードが表示されます。 - 信号品質と最後の通信時刻を確認して、インフラのノードが健全であることを確かめます。 絞り込みと並べ替えのオプションの詳細については、[ノード](nodes) を参照してください。 -## メッシュ探索のヒント +## Tips for Mesh exploration - **まずルート追跡から**:特定の経路について、すぐに役立つ情報が得られます。 - **主要なノードで隣接ノード情報を有効に**:特にルーターやリピーターで有効にして、バックボーンの全体像を組み立てます。 diff --git a/docs/ja-rJP/user/firmware.md b/docs/ja-rJP/user/firmware.md index 1626b8a3ca..e13ac51c36 100644 --- a/docs/ja-rJP/user/firmware.md +++ b/docs/ja-rJP/user/firmware.md @@ -1,6 +1,5 @@ --- title: ファームウェア更新 -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: 無線機のファームウェアを Bluetooth または USB で更新します。OTA の手順、バージョンチャンネル、事前チェック、復旧について説明します。 @@ -60,7 +59,7 @@ Wi-Fi OTA takes the ESP32 `-update.bin` image rather than the `.uf2` a USB updat ### アプリ内での USB 更新 -無線機が(Bluetooth ではなく)**USB/シリアル**で接続されている場合、ファームウェア更新画面に「**USB ファイル転送**」が表示されます。 アプリはデバイスを DFU モードで再起動し、システムのファイル選択画面を使って `.uf2` ファイルをデバイスの DFU ドライブに保存するよう促します。 このオプションは USB/シリアル接続でのみ表示され、Bluetooth では利用できません。 +無線機が(Bluetooth ではなく)**USB/シリアル**で接続されている場合、ファームウェア更新画面に「**USB ファイル転送**」が表示されます。アプリはデバイスを DFU モードで再起動し、システムのファイル選択画面を使って `.uf2` ファイルをデバイスの DFU ドライブに保存するよう促します。このオプションは USB/シリアル接続でのみ表示され、Bluetooth では利用できません。 > ℹ️ **Note:** A vendor nRF bootloader supplied as a `.zip` (e.g. RAK WisBlock RAK4631) has to be flashed with a serial DFU tool such as `adafruit-nrfutil` — copying that `.zip` to the drive won't work. A bootloader supplied as an `update-....uf2` **can** be installed by copying it to the drive; that is how the app's own bootloader upgrade works. The app surfaces a hint when the serial-only route applies. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ja-rJP/user/help-and-docs.md b/docs/ja-rJP/user/help-and-docs.md index b36917c428..75732a3fae 100644 --- a/docs/ja-rJP/user/help-and-docs.md +++ b/docs/ja-rJP/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: ヘルプとアプリ内ドキュメント -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: このドキュメントをアプリ内で閲覧・検索し、オンデバイスの AI アシスタント Chirpy に Meshtastic について質問できます。 @@ -13,11 +12,11 @@ aliases: # ヘルプとアプリ内ドキュメント -このユーザードキュメントは**アプリ内**にも同梱されており、Meshtastic を離れることなくオフラインで読めます。 「**設定 → ヘルプとドキュメント**」から開きます。 +このユーザードキュメントは**アプリ内**にも同梱されており、Meshtastic を離れることなくオフラインで読めます。「**設定 → ヘルプとドキュメント**」から開きます。 ## 閲覧する -ドキュメントブラウザーには、ユーザーガイドのすべてのページが一覧表示されます。 ページをタップすると読めます。画像や相互リンクも、ここと同じように機能します。 +ドキュメントブラウザーには、ユーザーガイドのすべてのページが一覧表示されます。ページをタップすると読めます。画像や相互リンクも、ここと同じように機能します。 ![アプリ内ドキュメントブラウザーの目次](../../assets/screenshots/docs-browser_toc.png) @@ -33,7 +32,7 @@ Tap the search field at the top of the docs browser to open a full-screen search ## Chirpy:AI アシスタント -**Chirpy** は、この同梱ドキュメントを情報源として、Meshtastic に関する平易な質問に答えます。 ドキュメントブラウザーで Chirpy ボタンをタップして質問を入力すると、回答と、関連ページへのリンクが返ってきます。 +**Chirpy** は、この同梱ドキュメントを情報源として、Meshtastic に関する平易な質問に答えます。ドキュメントブラウザーで Chirpy ボタンをタップして質問を入力すると、回答と、関連ページへのリンクが返ってきます。 ![ページリンク付きで質問に答える Chirpy AI アシスタント](../../assets/screenshots/docs-browser_chirpy.png) diff --git a/docs/ja-rJP/user/map-and-waypoints.md b/docs/ja-rJP/user/map-and-waypoints.md index da5ce2408a..c696ef7b2d 100644 --- a/docs/ja-rJP/user/map-and-waypoints.md +++ b/docs/ja-rJP/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: マップとウェイポイント -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: マップ上でノードの位置を確認し、ウェイポイントの作成・共有、マップレイヤーとサイトプランナーの管理、位置共有とプライバシーの制御を行います。 aliases: - map @@ -29,7 +28,7 @@ aliases: ### ノードのマーカー -位置を報告している各ノードは、そのノードの短縮名を表示する**ノードチップ**マーカーとして表示されます。 チップは、そのノード固有のアイデンティティカラー(ノード番号から導かれる一定の色)で色付けされます。ノードリストで使われるのと同じチップなので、どこでも同じ見た目になります。 マーカーの色は、オンライン/オフラインの状態を**表しません**。 ノードの位置がライブで更新されると、そのマーカーが短く脈打つように点滅します。 縮小すると、近くのマーカーはまとめて表示(クラスタリング)されます。 +位置を報告している各ノードは、そのノードの短縮名を表示する**ノードチップ**マーカーとして表示されます。チップは、そのノード固有のアイデンティティカラー(ノード番号から導かれる一定の色)で色付けされます。ノードリストで使われるのと同じチップなので、どこでも同じ見た目になります。マーカーの色は、オンライン/オフラインの状態を**表しません**。ノードの位置がライブで更新されると、そのマーカーが短く脈打つように点滅します。縮小すると、近くのマーカーはまとめて表示(クラスタリング)されます。 ### マップの操作 @@ -79,9 +78,9 @@ Waypoints always broadcast to the whole mesh on the primary channel. Unlike a me ウェイポイントは、自動的に期限切れになるよう設定できます: - **なし**(デフォルト):手動で削除するまでウェイポイントは残ります -- **期限付き**:特定の日時を指定します。その時刻を過ぎると、ウェイポイントは自動的に削除されます。 集合地点、危険箇所、待ち合わせ場所などの一時的なマーカーに便利です。 +- **期限付き**:特定の日時を指定します。その時刻を過ぎると、ウェイポイントは自動的に削除されます。集合地点、危険箇所、待ち合わせ場所などの一時的なマーカーに便利です。 -期限切れのウェイポイントは、表示が煩雑にならないよう、自動的にマップから隠されます。 有効期限のカウントダウンは、指定した絶対時刻を基準とし、ウェイポイントが作成または受信されてからの経過時間ではありません。 +期限切れのウェイポイントは、表示が煩雑にならないよう、自動的にマップから隠されます。有効期限のカウントダウンは、指定した絶対時刻を基準とし、ウェイポイントが作成または受信されてからの経過時間ではありません。 ### ウェイポイントのジオフェンス @@ -102,13 +101,13 @@ Waypoints always broadcast to the whole mesh on the primary channel. Unlike a me ## マップレイヤー -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. インポートしたレイヤーは、それぞれ表示/非表示を切り替えるトグルと、削除するオプションとともに一覧表示されます。 Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. ### サイトプランナー -**サイトプランナー**は、送信機の RF カバレッジを推定し、色分けされたオーバーレイとしてマップに描画します。 マップの操作から開くか、ノードの詳細ページから「**カバレッジを推定**」で開きます(位置が判明しているノードでのみ表示されます)。 送信機(位置、周波数、送信出力、アンテナ利得と高さ)、受信機(感度、高さ)、シミュレーションのオプション(最大範囲、高解像度の地形、カラーパレット)を設定してから、推定を実行します。 Like map layers, Site Planner works on both the Google Play and F-Droid builds, where the finished estimate is drawn on the map as a coverage overlay. On **Desktop** the same form is shown but the planner opens in your browser; to bring the estimate onto the map, click the transmitter pin in the browser, choose the planner's GeoJSON export, then add the downloaded file under **Manage Map Layers** with **Add Layer**. Use the GeoJSON export, not the KML one — the KML is a ground-overlay image this map cannot draw. +**サイトプランナー**は、送信機の RF カバレッジを推定し、色分けされたオーバーレイとしてマップに描画します。マップの操作から開くか、ノードの詳細ページから「**カバレッジを推定**」で開きます(位置が判明しているノードでのみ表示されます)。送信機(位置、周波数、送信出力、アンテナ利得と高さ)、受信機(感度、高さ)、シミュレーションのオプション(最大範囲、高解像度の地形、カラーパレット)を設定してから、推定を実行します。 Like map layers, Site Planner works on both the Google Play and F-Droid builds, where the finished estimate is drawn on the map as a coverage overlay. On **Desktop** the same form is shown but the planner opens in your browser; to bring the estimate onto the map, click the transmitter pin in the browser, choose the planner's GeoJSON export, then add the downloaded file under **Manage Map Layers** with **Add Layer**. Use the GeoJSON export, not the KML one — the KML is a ground-overlay image this map cannot draw. ## 位置の共有 @@ -125,7 +124,7 @@ Configure position behavior in **Settings → Device configuration → Position* ### プライバシーに関する注意 -> 🔒 **プライバシー:** 位置データは、チャンネル上のすべてのノードにブロードキャストされます。 位置を共有したくない場合は、設定で GPS 位置を無効にするか、固定/ダミーの位置を使用してください。 To keep sharing a position without pinpointing yourself, edit the channel in **Settings → Channels**, turn **Precise location** off, and set the slider beneath it — the channel then publishes an approximate area, shown as ± a distance, instead of an exact point. +> 🔒 **プライバシー:** 位置データは、チャンネル上のすべてのノードにブロードキャストされます。位置を共有したくない場合は、設定で GPS 位置を無効にするか、固定/ダミーの位置を使用してください。 To keep sharing a position without pinpointing yourself, edit the channel in **Settings → Channels**, turn **Precise location** off, and set the slider beneath it — the channel then publishes an approximate area, shown as ± a distance, instead of an exact point. ## マップソース @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ja-rJP/user/messages-and-channels.md b/docs/ja-rJP/user/messages-and-channels.md index c79960a717..034257bdfb 100644 --- a/docs/ja-rJP/user/messages-and-channels.md +++ b/docs/ja-rJP/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: メッセージとチャンネル -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: メッセージの送受信、チャンネルの管理、暗号化の設定、会話の検索、クイックチャット・リアクション・メッセージ操作の使い方を説明します。 @@ -17,7 +16,7 @@ Meshtastic は、**チャンネルブロードキャスト**と**ダイレクト ## チャンネル -チャンネルは、共有された通信グループです。 同じチャンネル鍵を設定したすべてのノードが、そのチャンネルでメッセージを読み書きできます。 +チャンネルは、共有された通信グループです。同じチャンネル鍵を設定したすべてのノードが、そのチャンネルでメッセージを読み書きできます。 ### デフォルトチャンネル @@ -124,7 +123,7 @@ control whether they are offered at all. | エラー | 意味 | 対処方法 | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| ルートがありません | 宛先ノードへの経路が存在しません | 受信者がオフラインか、メッシュの範囲外の可能性があります。 時間をおくか、距離を縮めてください。 | +| ルートがありません | 宛先ノードへの経路が存在しません | 受信者がオフラインか、メッシュの範囲外の可能性があります。時間をおくか、距離を縮めてください。 | | No radio interface | 送信に使える無線インターフェースがありません | Check that your radio is connected and available. | | メッシュへの配信に失敗しました | Retries exhausted. The same label covers three underlying causes — a relay refusing (NAK), a plain timeout, and running out of retransmits | Move closer, improve signal, or wait for conditions to improve. Tap the error for the specific cause. | | Rate limited | The mesh is throttling you for sending too fast | Wait before sending again. | @@ -140,7 +139,7 @@ control whether they are offered at all. | Duty cycle limit | 地域ごとの電波利用時間の上限に達しました | Wait for the duty cycle window to reset. | | Invalid request | Malformed or invalid request | Retry after updating or restarting the app if this persists. | -> 💡 **ヒント:** ほとんどの配信エラーは自然に解消します。 ノードに断続的に到達できる場合、メッシュは再試行します。 For persistent **No route** errors, check that intermediate Router nodes are online. +> 💡 **ヒント:** ほとんどの配信エラーは自然に解消します。ノードに断続的に到達できる場合、メッシュは再試行します。 For persistent **No route** errors, check that intermediate Router nodes are online. ## メッセージの機能 @@ -164,20 +163,20 @@ Each quick chat entry has a **Name** — the button label, capped at five charac 1. 会話(チャンネルまたはダイレクトメッセージ)を開きます。 2. 上部バーの**検索アイコン**をタップします。 -3. 「**メッセージを検索…**」欄に入力します。 検索は入力に応じて、その会話に保存されているすべてのメッセージを対象に実行されます。 +3. 「**メッセージを検索…**」欄に入力します。検索は入力に応じて、その会話に保存されているすべてのメッセージを対象に実行されます。 4. **N/M** の結果カウンターと**前へ/次への矢印**を使って、一致箇所間を移動できます。一致箇所は会話内でハイライト表示されます。 ![結果カウンターと前へ/次への矢印を備えたメッセージ検索バー](../../assets/screenshots/messages_search_bar.png) -> 💡 **ヒント:** 検索は全文検索で、開いた会話の中だけを対象とします。他のチャンネルや連絡先をまたいで検索することはありません。 デバイスにすでに保存されているメッセージを対象に照合するため、完全にオフラインで動作します。 +> 💡 **ヒント:** 検索は全文検索で、開いた会話の中だけを対象とします。他のチャンネルや連絡先をまたいで検索することはありません。デバイスにすでに保存されているメッセージを対象に照合するため、完全にオフラインで動作します。 ### メッセージの吹き出し -メッセージはチャットの吹き出しとして表示され、送信メッセージは右側、受信メッセージは左側に並びます。 各吹き出しには、送信者・タイムスタンプ・配信状況が表示されます。 返信付きのメッセージでは、応答の上に元メッセージの引用プレビューが表示されます。 +メッセージはチャットの吹き出しとして表示され、送信メッセージは右側、受信メッセージは左側に並びます。各吹き出しには、送信者・タイムスタンプ・配信状況が表示されます。返信付きのメッセージでは、応答の上に元メッセージの引用プレビューが表示されます。 ### テキストの書式 -メッセージは、軽量なインライン **Markdown** に対応しています。 受信したメッセージは、記法の文字が取り除かれた状態でスタイルが適用されて表示されます: +メッセージは、軽量なインライン **Markdown** に対応しています。受信したメッセージは、記法の文字が取り除かれた状態でスタイルが適用されて表示されます: | 種別 | 記法 | 表示結果 | | -------- | ------------------------------ | ------------- | @@ -187,13 +186,13 @@ Each quick chat entry has a **Name** — the button label, capped at five charac | インラインコード | `` `code` `` | 等幅の `code` | | リンク | `[label](https://example.com)` | タップできる**ラベル** | -メッセージを作成するときは、入力欄にフォーカスして 3 文字以上入力すると、入力欄の下に**書式ツールバー**が表示されます。 テキストを選択してスタイルをタップすると、そのテキストが囲まれます(もう一度タップすると解除されます)。選択していない場合は、空のペアが挿入され、カーソルがマーカーの間に置かれます。 リンクボタンをタップすると、URL を入力するダイアログが開きます。 As you type, the field shows the styled text, but the message you send still contains the Markdown characters. +メッセージを作成するときは、入力欄にフォーカスして 3 文字以上入力すると、入力欄の下に**書式ツールバー**が表示されます。テキストを選択してスタイルをタップすると、そのテキストが囲まれます(もう一度タップすると解除されます)。選択していない場合は、空のペアが挿入され、カーソルがマーカーの間に置かれます。リンクボタンをタップすると、URL を入力するダイアログが開きます。 As you type, the field shows the styled text, but the message you send still contains the Markdown characters. > 💡 **ヒント:** 書式はメッシュ上ではそのままの文字として送られます。iOS が送信するのと同じバイト列です。 Markdown に対応していないクライアント(古いアプリや、素のファームウェアのクライアント)では、`**` や `~~` の文字がそのまま表示されます。 URL、メールアドレス、電話番号は、Markdown を使うかどうかにかかわらず、引き続き自動的にリンクになります。 ### メンション -メッセージ作成中に `@` を入力するとノードにメンションできます。入力に応じて、一致する連絡先がピッカーに提案されます。 受信メッセージでは、メンションはノード名を表示したハイライト付きのチップとして現れます。タップすると、そのノードの詳細ページに直接移動できます。 +メッセージ作成中に `@` を入力するとノードにメンションできます。入力に応じて、一致する連絡先がピッカーに提案されます。受信メッセージでは、メンションはノード名を表示したハイライト付きのチップとして現れます。タップすると、そのノードの詳細ページに直接移動できます。 ### リアクション diff --git a/docs/ja-rJP/user/mqtt.md b/docs/ja-rJP/user/mqtt.md index e482c5f93e..9cfab75f57 100644 --- a/docs/ja-rJP/user/mqtt.md +++ b/docs/ja-rJP/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: メッシュをインターネットに橋渡しします。MQTT サーバーの設定、暗号化の各レイヤー、マップ報告について説明します。 aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | 無効 | | **TLS enabled** | サーバーへのセキュアな接続 | 無効 | | **Map reporting** | 公開マップに位置を報告 | 無効 | -| **Proxy to client enabled** | Relay MQTT through the connected phone | 無効 | +| **Proxy to client enabled** | Relay MQTT through the connected app | 無効 | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,21 +62,19 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### このスマートフォンでの MQTT プロキシ +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### デフォルトの Meshtastic サーバー -コミュニティが `mqtt.meshtastic.org` で公開サーバーを運用しています。 これは一般的な利用やテストを目的としています。 +コミュニティが `mqtt.meshtastic.org` で公開サーバーを運用しています。これは一般的な利用やテストを目的としています。 -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). -> 🔒 **プライバシー:** 公開サーバー上のメッセージは、購読している誰もが読めます。 プライベートな通信には、必ずチャンネルの暗号化を使用してください。 +> 🔒 **プライバシー:** 公開サーバー上のメッセージは、購読している誰もが読めます。プライベートな通信には、必ずチャンネルの暗号化を使用してください。 ### プライベートサーバー @@ -93,7 +90,7 @@ When this phone relays MQTT for the radio, connections to that broker always use When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. @@ -125,11 +122,11 @@ MQTT carries two payload formats: 階層化された暗号化モデルを理解する: -1. **チャンネルの暗号化**は、MQTT の_前に_メッシュ上で行われます。 チャンネルに PSK が設定されている場合、MQTT ペイロードはすでに暗号化されています。サーバーや購読者には暗号文しか見えません。 +1. **チャンネルの暗号化**は、MQTT の_前に_メッシュ上で行われます。チャンネルに PSK が設定されている場合、MQTT ペイロードはすでに暗号化されています。サーバーや購読者には暗号文しか見えません。 2. **Encryption enabled** (the module setting) decides which copy of the packet the gateway publishes — it is not an extra layer. Leave it on and the broker receives the packet still encrypted with your channel key. Turn it off and the gateway publishes the decrypted packet, so anyone subscribed to the topic reads your messages in the clear. Turn it off only when you own the broker and want plain payloads for a dashboard. 3. **TLS** はサーバーへの TCP 接続自体を暗号化し、ネットワークレベルの盗聴を防ぎます。 -> 🔒 **Security:** The default public channel has a well-known key. デフォルトチャンネルで MQTT 経由で送信されるメッセージは、実質的に**暗号化されていません**。誰でも解読できます。 プライベートな通信には、必ずカスタムの PSK を使用してください。 +> 🔒 **Security:** The default public channel has a well-known key. デフォルトチャンネルで MQTT 経由で送信されるメッセージは、実質的に**暗号化されていません**。誰でも解読できます。プライベートな通信には、必ずカスタムの PSK を使用してください。 ## ベストプラクティス @@ -146,12 +143,12 @@ MQTT carries two payload formats: - **Check Wi-Fi** — the gateway node must have an active internet connection (Wi-Fi or Ethernet). MQTT は LoRa の無線リンク自体では動作しません。 - **Verify credentials** — with incorrect credentials, most brokers fail silently — double-check for trailing spaces. - **Firewall** — port 1883 (MQTT) or 8883 (MQTT over TLS) must be reachable. Some networks allow only web traffic (ports 80 and 443). -- **DNS 解決:** カスタムのサーバーホスト名を使う場合は、ノードがそれを解決できるか確認してください。 サーバーの IP アドレスを直接試してみてください。 +- **DNS 解決:** カスタムのサーバーホスト名を使う場合は、ノードがそれを解決できるか確認してください。サーバーの IP アドレスを直接試してみてください。 ### メッセージが橋渡しされない -- **アップリンク/ダウンリンクの設定を確認:** アップリンクのみが有効な場合、メッセージはメッシュから MQTT へ流れますが、戻ってきません。 受信側のゲートウェイでダウンリンクを有効にしてください。 -- **チャンネルの不一致:** 両方のゲートウェイが、同じ PSK を持つ同じチャンネルを共有している必要があります。 不一致の場合、メッセージは異なる鍵で暗号化され、判読できないデータとして表示されます。 +- **アップリンク/ダウンリンクの設定を確認:** アップリンクのみが有効な場合、メッセージはメッシュから MQTT へ流れますが、戻ってきません。受信側のゲートウェイでダウンリンクを有効にしてください。 +- **チャンネルの不一致:** 両方のゲートウェイが、同じ PSK を持つ同じチャンネルを共有している必要があります。不一致の場合、メッセージは異なる鍵で暗号化され、判読できないデータとして表示されます。 - **Topic mismatch** — both gateways must use exactly the same root topic. Setting a region rewrites a default root to `msh/` (for example `msh/US`), so gateways in different regions do not meet until you give both the same explicit root. - **Ignore MQTT is on** — in a region with a duty-cycle limit, the radio turns on **Ignore MQTT** (LoRa config, **Advanced**) when you set the region, and then drops every packet that reached it via MQTT. Turn it off on the receiving nodes, not only on the gateway. - **Ok to MQTT is off** — on a public broker a gateway uplinks other nodes' packets only when the sending node has **Ok to MQTT** (LoRa config, **Advanced**) on. Your own traffic bridges either way; your neighbors' does not until they opt in. diff --git a/docs/ja-rJP/user/node-metrics.md b/docs/ja-rJP/user/node-metrics.md index db06b308eb..2231795bce 100644 --- a/docs/ja-rJP/user/node-metrics.md +++ b/docs/ja-rJP/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: ノードメトリクス -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: 各メッシュノードのテレメトリダッシュボード。デバイスの状態、環境センサー、大気質、信号品質、電力、ルート追跡、位置履歴を表示します。 aliases: - metrics @@ -64,11 +63,11 @@ BME680 の \*\*IAQ(室内空気質)\*\*指数は、ガス抵抗から算出 ![「非常に良い」から「危険なほど汚染」までの IAQ 指数スケール](../../assets/screenshots/node-metrics_iaq_scale.png) -> 💡 **ヒント:** 環境メトリクスには、リモートノードに接続されたセンサーが必要です。 すべてのノードが環境データを報告するわけではありません。 対応センサーの一覧については、[テレメトリとセンサー](telemetry-and-sensors) を参照してください。 +> 💡 **ヒント:** 環境メトリクスには、リモートノードに接続されたセンサーが必要です。すべてのノードが環境データを報告するわけではありません。対応センサーの一覧については、[テレメトリとセンサー](telemetry-and-sensors) を参照してください。 ## 大気質メトリクス -大気質は、粒子状物質センサーや CO₂ センサーを搭載したノード向けの専用メトリクスビューです。 これは、環境メトリクスに記載されている **BME680 の IAQ の測定値とは別のもの**です。IAQ はガス抵抗から算出される単一の指数であるのに対し、大気質ビューはその基となる粒子状物質と CO₂ の測定値をグラフ化します。 +大気質は、粒子状物質センサーや CO₂ センサーを搭載したノード向けの専用メトリクスビューです。これは、環境メトリクスに記載されている **BME680 の IAQ の測定値とは別のもの**です。IAQ はガス抵抗から算出される単一の指数であるのに対し、大気質ビューはその基となる粒子状物質と CO₂ の測定値をグラフ化します。 | メトリクス | 単位 | 説明 | | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -92,14 +91,14 @@ CO₂ readings are color-coded by severity so you can read air quality at a glan ![CO₂ の深刻度が色分けされた大気質の測定値](../../assets/screenshots/node-metrics_air_quality.png) -大気質のログ/メトリクスボタンは、**ノードが大気質のテレメトリを報告したときにのみ**、ノードの詳細画面に表示されます。 大気質ビューでは、次のことができます: +大気質のログ/メトリクスボタンは、**ノードが大気質のテレメトリを報告したときにのみ**、ノードの詳細画面に表示されます。大気質ビューでは、次のことができます: - グラフの**期間**を選択します。 - **メトリクスチップ**で絞り込みます。データがあるメトリクスのみが表示されます。 - 最新の大気質テレメトリを**更新/要求**します。 - 表計算ソフトで分析できるよう、**CSV にエクスポート**します。 -> 💡 **ヒント:** 大気質メトリクスには、リモートノードに対応する大気質センサーが必要です。 対応ハードウェアについては、[テレメトリとセンサー](telemetry-and-sensors) を参照してください。 +> 💡 **ヒント:** 大気質メトリクスには、リモートノードに対応する大気質センサーが必要です。対応ハードウェアについては、[テレメトリとセンサー](telemetry-and-sensors) を参照してください。 ## 信号品質 @@ -125,7 +124,7 @@ CO₂ readings are color-coded by severity so you can read air quality at a glan 詳しい説明は、[信号メーターを理解する](signal-meter) を参照してください。 -接続中の無線機のローカル統計も、利用可能な場合は信号品質に表示されます。 これらのログには、ノイズフロア、トラフィックカウンター、中継カウンター、オンラインノード数、無線機の連続稼働時間が含まれます。 ノイズフロアのグラフでは、混雑した RF 環境を見分けやすいよう、-85 dBm に破線の基準線が引かれます。 +接続中の無線機のローカル統計も、利用可能な場合は信号品質に表示されます。これらのログには、ノイズフロア、トラフィックカウンター、中継カウンター、オンラインノード数、無線機の連続稼働時間が含まれます。ノイズフロアのグラフでは、混雑した RF 環境を見分けやすいよう、-85 dBm に破線の基準線が引かれます。 - **Request** — ask the connected radio for a fresh Local Stats telemetry report - **Clear** — remove Local Stats logs for that node @@ -152,7 +151,7 @@ The node detail screen shows cards for channels 1 to 3. Use the chart button on ### ルート追跡の結果の見方 -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ja-rJP/user/nodes.md b/docs/ja-rJP/user/nodes.md index 0a711a8fa9..226b0509f3 100644 --- a/docs/ja-rJP/user/nodes.md +++ b/docs/ja-rJP/user/nodes.md @@ -1,8 +1,7 @@ --- title: ノード -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: メッシュノードの閲覧・絞り込み・並べ替え。詳細、信号品質、役割、クイック操作を確認できます。 aliases: - node-list @@ -15,62 +14,66 @@ aliases: The Nodes screen lists every node visible on your mesh. -## ノードリスト +## Node list -ノードリストには、無線機が受信したすべてのノードが、次の情報とともに表示されます: +The node list shows every node your node has heard, including: - **ノード名:** ユーザーが設定した正式名称 - **短縮名:** 4 文字の識別子 -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **最後の通信:** 最後に通信してからの経過時間 - **距離:** 推定距離(位置情報が共有されている場合) - **バッテリー:** リモートノードのバッテリー残量(テレメトリが有効な場合) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### ノードのステータス表示 +### Node Status indicators -| 指標 | 意味 | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | 過去 2 時間以内に受信したノード | -| Plain last-heard time | 2 時間を超えて受信していないノード | -| ⭐ お気に入り | Node you marked as a favorite. | +| 指標 | 意味 | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | 過去 2 時間以内に受信したノード | +| Plain last-heard time | 2 時間を超えて受信していないノード | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ お気に入り | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### ノードの役割 +### Node roles ノードには、メッシュ上での動作に影響するさまざまな役割を設定できます: -| 役割 | 説明 | -| --------------- | -------------------------------------------------------------------------------------------------------------- | -| クライアント | Standard end-user node | -| クライアント・ベース | お気に入りノードの通信をルーター・レイトの優先度で扱い、それ以外の通信はクライアントとして扱います | -| クライアント・ミュート | 受信しますが、再送信はしません | -| クライアント・非表示 | クライアント・ミュートと同様で、さらにノードリストから隠れます | -| ルーター | メッセージの転送を優先し、中継のために起動し続けます | -| ルーター・レイト | 1 回だけ再送信するインフラノードですが、他のすべてのモードの後にのみ行います(補助的なカバレッジを提供します) | -| ~~ルーター・クライアント~~ | ⚠️ **非推奨**(ファームウェア 2.3.15 で削除)。選択できなくなりました。代わりにルーターまたはクライアントを使用してください | -| ~~リピーター~~ | ⚠️ **非推奨**(ファームウェア 2.7.11 で削除)。選択できなくなりました。代わりにルーターを使用してください | -| トラッカー | 一定間隔での位置報告に最適化されています | -| センサー | テレメトリ報告に最適化されています | -| TAK | TAK システムと相互運用します(CoT を送受信) | -| TAK Tracker | TAK の位置報告のみ | -| 紛失モード | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| 役割 | 説明 | +| --------------- | ------------------------------------------------------------------------------------------------------------- | +| クライアント | Standard end-user node | +| クライアント・ベース | お気に入りノードの通信をルーター・レイトの優先度で扱い、それ以外の通信はクライアントとして扱います | +| クライアント・ミュート | 受信しますが、再送信はしません | +| クライアント・非表示 | クライアント・ミュートと同様で、さらにノードリストから隠れます | +| ルーター | メッセージの転送を優先し、中継のために起動し続けます | +| ルーター・レイト | 1 回だけ再送信するインフラノードですが、他のすべてのモードの後にのみ行います(補助的なカバレッジを提供します) | +| ~~ルーター・クライアント~~ | ⚠️ **非推奨**(ファームウェア 2.3.15 で削除)。選択できなくなりました。代わりにルーターまたはクライアントを使用してください | +| ~~リピーター~~ | ⚠️ **非推奨**(ファームウェア 2.7.11 で削除)。選択できなくなりました。代わりにルーターを使用してください | +| トラッカー | 一定間隔での位置報告に最適化されています | +| センサー | テレメトリ報告に最適化されています | +| TAK | TAK システムと相互運用します(CoT を送受信) | +| TAK Tracker | TAK の位置報告のみ | +| 紛失モード | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### 役割を選ぶ +### Choosing a role -ほとんどのユーザーは、デフォルトの**クライアント**役割のままにしてください。 次のような場合は、別の役割を検討してください: +ほとんどのユーザーは、デフォルトの**クライアント**役割のままにしてください。次のような場合は、別の役割を検討してください: -- **ルーター:** 電源が安定した、固定された高所(屋上、丘の上など)にノードを設置している場合。 ルーターは他のノードのメッセージを中継するために常に起動し続け、メッシュのカバレッジ拡大に不可欠です。 Don't use Router on battery-powered handheld radios. -- **ルーター・レイト:** パケットを常に 1 回だけ再送信するインフラノードですが、他のすべてのルーティングモードの番が終わった後にのみ行います。 主要なルーターと競合することなく、ローカルのクラスターに補助的なカバレッジを提供します。 +- **ルーター:** 電源が安定した、固定された高所(屋上、丘の上など)にノードを設置している場合。ルーターは他のノードのメッセージを中継するために常に起動し続け、メッシュのカバレッジ拡大に不可欠です。 Don't use Router on battery-powered handheld nodes. +- **ルーター・レイト:** パケットを常に 1 回だけ再送信するインフラノードですが、他のすべてのルーティングモードの番が終わった後にのみ行います。主要なルーターと競合することなく、ローカルのクラスターに補助的なカバレッジを提供します。 - **クライアント・ベース:** お気に入りノードとの間の通信をルーター・レイトの優先度で扱い(それらのメッセージが追加の中継カバレッジを得られるようにします)、それ以外はすべて通常のクライアントとして処理します。 -- **クライアント・ミュート:** メッシュの通信を受信したいが、中継には貢献したくない場合。 Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). 発信の合間はスリープしてバッテリーを節約します。 -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). 電力プロファイルはトラッカーと同様です。 -- **TAK/TAK Tracker:** ATAK/WinTAK システムと相互運用する場合にのみ必要です。 詳しくは [TAK 連携](tak) を参照してください。 +- **クライアント・ミュート:** メッシュの通信を受信したいが、中継には貢献したくない場合。 Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). 発信の合間はスリープしてバッテリーを節約します。 +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). 電力プロファイルはトラッカーと同様です。 +- **TAK/TAK Tracker:** ATAK/WinTAK システムと相互運用する場合にのみ必要です。詳しくは [TAK 連携](tak) を参照してください。 > 💡 **ヒント:** メッシュは、ほとんどのノードが**クライアント**または**ルーター**のときに最も良く機能します。 Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. 目安としては、地域内のクライアント 5〜10 台につきルーター 1 台です。 @@ -78,19 +81,19 @@ There is no separate "away" tier. Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| アイコン | 意味 | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ 不一致 | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| アイコン | 意味 | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ 不一致 | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## クイック操作 @@ -107,10 +110,10 @@ A mismatch never replaces the key you already hold. The app keeps the first key Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,44 +121,62 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## 絞り込みと並べ替え +## Filtering & sorting -### テキスト検索 +### Text search -検索欄に入力すると、名前または短縮名でノードを絞り込めます。 絞り込みは、入力に応じてリアルタイムで更新されます。 +検索欄に入力すると、名前または短縮名でノードを絞り込めます。絞り込みは、入力に応じてリアルタイムで更新されます。 -### 絞り込みトグル +### Filter toggles -| 絞り込み | 説明 | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | 過去 2 時間以内に受信したノードのみを表示します | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **不明なノードを含む** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **インフラを除外** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **MQTT を除外** | MQTT のインターネットブリッジ経由でのみ受信したノードを非表示にします | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 絞り込み | 説明 | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | 過去 2 時間以内に受信したノードのみを表示します | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **不明なノードを含む** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **インフラを除外** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **MQTT を除外** | MQTT のインターネットブリッジ経由でのみ受信したノードを非表示にします | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### 並べ替えオプション +### Nodes not heard on your current Settings -| 並べ替え | 説明 | -| --------------------------------------------- | ------------------------------------ | -| **Last heard** | 最近受信したノードを先頭に表示します | -| **A-Z** | ノードの正式名称で並べ替えます | -| **距離** | 近いノードを先頭に表示します(位置情報の共有が必要) | -| **ホップ数** | 中継ホップ数が少ないノードを先頭に表示します | -| **チャンネル** | チャンネルインデックスごとにグループ化します | -| **via MQTT** | MQTT 経由と無線受信でグループ化します | -| **via Favorite** (default) | Favorited nodes first, then the rest | +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. -## ホップごとのノード数 +The banner offers two actions: -ノードリストのアプリバーにあるホップヒストグラムのアイコンをタップすると、各ホップ距離にいくつのノードがあるかを示す棒グラフが開きます(0 = 直接、1 = 中継 1 回、以下同様)。 グラフを**最後の通信**の期間(すべての期間、1 時間、8 時間、24 時間)で絞り込むと、現在のメッシュの様子と、より長い期間での様子を比較できます。 ローカルメッシュがどれだけ混雑し、どれだけ広がっているかを手早く把握できます。 +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. -## ノードの詳細 +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. -ノードをタップすると、詳しい情報を含む詳細ビューが開きます。 メトリクスとテレメトリの詳細については、[ノードメトリクス](node-metrics) を参照してください。 +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options + +| 並べ替え | 説明 | +| --------------------------------------------- | ---------------------------------------------- | +| **Last heard** | 最近受信したノードを先頭に表示します | +| **A-Z** | ノードの正式名称で並べ替えます | +| **距離** | 近いノードを先頭に表示します(位置情報の共有が必要) | +| **ホップ数** | 中継ホップ数が少ないノードを先頭に表示します | +| **チャンネル** | チャンネルインデックスごとにグループ化します | +| **via MQTT** | Grouped by MQTT vs. node-heard | +| **via Favorite** (default) | Favorited nodes first, then the rest | + +## Nodes per hop + +ノードリストのアプリバーにあるホップヒストグラムのアイコンをタップすると、各ホップ距離にいくつのノードがあるかを示す棒グラフが開きます(0 = 直接、1 = 中継 1 回、以下同様)。 Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. ローカルメッシュがどれだけ混雑し、どれだけ広がっているかを手早く把握できます。 + +## Node detail + +ノードをタップすると、詳しい情報を含む詳細ビューが開きます。メトリクスとテレメトリの詳細については、[ノードメトリクス](node-metrics) を参照してください。 Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. @@ -173,20 +194,32 @@ The Details card carries the node's short name, role, IDs, last heard time, hops | 最後の通信 | ![最後の通信](../../assets/screenshots/nodes_last_heard.png) | | 距離 | ![距離](../../assets/screenshots/nodes_distance_info.png) | -### デバイスのリンク(「購入はこちら」) +### Hardware support status -ノードのハードウェアが認識されると、詳細ビューに折りたたみ式の\*\*「購入はこちら」\*\*セクションが表示され、そのデバイスを購入したり詳しく知ったりできる場所(ベンダーの製品ページ、製品バリエーション、AliExpress・Amazon・対応小売店などの地域のマーケットプレイスの掲載)が、あなたの国に合わせて絞り込まれて表示されます。 各リンクは `msh.to` のリダイレクトサービスを通じて開きます。 一致するリンクがないデバイスでは、このセクションは表示されません。 +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | 意味 | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") + +ノードのハードウェアが認識されると、詳細ビューに折りたたみ式の\*\*「購入はこちら」\*\*セクションが表示され、そのデバイスを購入したり詳しく知ったりできる場所(ベンダーの製品ページ、製品バリエーション、AliExpress・Amazon・対応小売店などの地域のマーケットプレイスの掲載)が、あなたの国に合わせて絞り込まれて表示されます。各リンクは `msh.to` のリダイレクトサービスを通じて開きます。一致するリンクがないデバイスでは、このセクションは表示されません。 A full, browsable directory of every link is also available at **Settings → Device Links**. The item is hidden while you have Settings open for a remote node. Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## 関連トピック diff --git a/docs/ja-rJP/user/notifications.md b/docs/ja-rJP/user/notifications.md new file mode 100644 index 0000000000..3c65062c38 --- /dev/null +++ b/docs/ja-rJP/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: 通知 +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# 通知 + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----- | ----------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| メッセージ | ダイレクトメッセージの通知 | A message sent directly to you | The conversation | +| メッセージ | ブロードキャストメッセージの通知 | A message on one of your channels | The channel | +| メッセージ | ウェイポイントの通知 | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| メッセージ | アラート通知 | A critical alert from a node | The conversation | +| メッシュ | 新しいノードの通知 | A node heard for the first time | The node's details | +| メッシュ | メッシュ招待の通知 | An invitation to join a nearby mesh | ローカルメッシュ探索 | +| メッシュ | バッテリー残量低下通知 (お気に入りノード) | A favorite node's battery running low | The node's details | +| デバイス | サービス通知 | The connection to your node while the app runs in the background | The app | +| デバイス | バッテリー残量低下通知 | Your node's battery running low | The node's details | +| デバイス | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| デバイス | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## 関連トピック + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ja-rJP/user/onboarding.md b/docs/ja-rJP/user/onboarding.md index bed4ad9528..83b3846313 100644 --- a/docs/ja-rJP/user/onboarding.md +++ b/docs/ja-rJP/user/onboarding.md @@ -1,8 +1,7 @@ --- title: はじめに -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: 初回起動時のセットアップ:権限、オンボーディングの流れ、無線機を接続した後の次のステップ。 aliases: - 初回起動 @@ -34,7 +33,7 @@ Tap **Get started** to proceed through the setup flow. ## 権限 -アプリはセットアップ中にいくつかの権限を要求します。 それぞれに特定の目的があり、一部は主要な機能に必要です。 +アプリはセットアップ中にいくつかの権限を要求します。それぞれに特定の目的があり、一部は主要な機能に必要です。 ### Bluetooth の権限 @@ -55,6 +54,8 @@ Meshtastic は、次の目的でも位置情報を使用します: - 他のノードまでの距離を計算する - 他のメッシュメンバーと GPS 座標を共有する(有効な場合) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ja-rJP/user/settings-module-admin.md b/docs/ja-rJP/user/settings-module-admin.md index 5cd055c33a..70ecb2a312 100644 --- a/docs/ja-rJP/user/settings-module-admin.md +++ b/docs/ja-rJP/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: 設定:モジュールと管理 -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: オプションの機能モジュール(MQTT、テレメトリ、定型メッセージ、TAK など)を設定し、デバイスの管理を行います。 aliases: - modules @@ -12,9 +11,9 @@ aliases: # 設定:モジュールと管理 -オプションの機能モジュールを設定し、デバイスの管理を行います。 モジュールは、専用の機能で Meshtastic を拡張します。それぞれ個別に有効・無効を切り替えられます。 +オプションの機能モジュールを設定し、デバイスの管理を行います。モジュールは、専用の機能で Meshtastic を拡張します。それぞれ個別に有効・無効を切り替えられます。 -> 💡 **ヒント:** 実際に使うモジュールだけを有効にすれば十分です。 使わないモジュールを無効にすると、電波利用時間が減り、バッテリーを節約でき、設定もシンプルになります。 A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **ヒント:** 実際に使うモジュールだけを有効にすれば十分です。使わないモジュールを無効にすると、電波利用時間が減り、バッテリーを節約でき、設定もシンプルになります。 A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. モジュールの設定は、トグルスイッチ、ドロップダウン、テキストフィールド、スライダーを備えたカード形式のレイアウトを使用します: @@ -26,45 +25,45 @@ aliases: ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## モジュールの設定 +## 追加機能の設定 Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT モジュール +### MQTT module -インターネット接続のために、メッシュのメッセージを MQTT サーバーとの間で橋渡しします。 これにより、無線の到達範囲を超えてメッシュを拡張したり、ホームオートメーションシステムと連携したりできます。 +インターネット接続のために、メッシュのメッセージを MQTT サーバーとの間で橋渡しします。 This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| 設定項目 | 説明 | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTTを有効化 | MQTT ブリッジを切り替え | -| アドレス | MQTT サーバーのアドレス | -| ユーザー名 | 認証用のユーザー名 | -| パスワード | 認証用のパスワード | -| 暗号化の有効化 | MQTT ペイロードを暗号化 | -| JSON出力の有効化 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS の有効化 | セキュアな接続を使用 | -| ルート トピック | MQTT のベーストピックパス | -| クライアントへのプロキシの有効化 | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| このスマートフォンの MQTT プロキシ | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| マップレポート | Publish position to the public map — see the Map reporting group that follows | +| 設定項目 | 説明 | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTTを有効化 | MQTT ブリッジを切り替え | +| アドレス | MQTT サーバーのアドレス | +| ユーザー名 | 認証用のユーザー名 | +| パスワード | 認証用のパスワード | +| 暗号化の有効化 | MQTT ペイロードを暗号化 | +| JSON出力の有効化 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS の有効化 | セキュアな接続を使用 | +| ルート トピック | MQTT のベーストピックパス | +| クライアントへのプロキシの有効化 | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| マップレポート | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| 設定項目 | 説明 | -| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 同意します。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| マップレポートの間隔 (秒) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| 設定項目 | 説明 | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 同意します。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | 暗号化、プライバシー、サーバーの設定を含む詳しい使い方は、[MQTT](mqtt) を参照してください。 -### シリアルモジュール +### Serial module -外部デバイスとの連携(GPS モジュール、センサー、カスタムハードウェア)のために、シリアルポート通信を有効にします。 有効にすると、ノードのシリアルポートで protobuf またはテキストデータを送受信でき、外部のマイコンやコンピューターがメッシュとやり取りできるようになります。 +外部デバイスとの連携(GPS モジュール、センサー、カスタムハードウェア)のために、シリアルポート通信を有効にします。有効にすると、ノードのシリアルポートで protobuf またはテキストデータを送受信でき、外部のマイコンやコンピューターがメッシュとやり取りできるようになります。 | 設定項目 | 説明 | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -76,9 +75,9 @@ until you agree: | タイムアウト | How long to wait before considering an incoming message complete | | コンソールのシリアルポートを上書き | Take over the port the debug console normally uses | -### 外部通知モジュール +### External Notification module -無線機ハードウェアのブザー、LED、振動によるアラートを制御します。 メッセージ到着時に物理的に知らせる必要があるデバイスに便利です。特に無人設置や屋外設置で役立ちます。 +Controls buzzer, LED, or vibration alerts on your node hardware. メッセージ到着時に物理的に知らせる必要があるデバイスに便利です。特に無人設置や屋外設置で役立ちます。 There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,13 +93,13 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | バイブレーション出力 (GPIO) | Pin the vibration motor is wired to | | PWMブザーを使用 | Drive the buzzer with PWM, which allows tones rather than a single pitch | | I2Sをブザーとして使用 | Send the alert through an I2S audio output instead | -| 出力時間 (ミリ秒) | How long a single alert lasts | -| 繰り返し通知間隔(秒) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | 着信音 | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward モジュール +### Store & Forward module -一時的にオフラインだったノードのためにメッセージをバッファリングし、それらのノードが再接続したときに再送します。 ノードが頻繁に圏内・圏外を行き来するメッシュに不可欠です。短時間の切断中にメッセージが失われないようにします。 +一時的にオフラインだったノードのためにメッセージをバッファリングし、それらのノードが再接続したときに再送します。ノードが頻繁に圏内・圏外を行き来するメッシュに不可欠です。短時間の切断中にメッセージが失われないようにします。 | 設定項目 | 説明 | | ------------------------------------------------------ | ------------------------------------------------------ | @@ -111,49 +110,49 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | リクエスト可能な履歴の期間 (分) | 再送する時間の範囲 | | サーバー | メッシュのストア&フォワードサーバーとして動作する(十分なメモリが必要、例:PSRAM 搭載の ESP32) | -> 💡 **ヒント:** ストア&フォワードは、十分なメモリを持つノード(PSRAM 搭載の ESP32)で最も良く機能します。 ルーターノードは通常は常時起動しているため、理想的な候補です。 +> 💡 **ヒント:** ストア&フォワードは、十分なメモリを持つノード(PSRAM 搭載の ESP32)で最も良く機能します。ルーターノードは通常は常時起動しているため、理想的な候補です。 -### レンジテストモジュール +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still > switch an already-enabled module off — and saving force-disables the module if the channel has > reverted to public. -ノード間のリンク品質を評価するための、自動レンジテストツールです。 有効にすると、ノードはカウンターを増やしながらテストメッセージを定期的に送信します。 受信側のノードがこれらのメッセージを記録するため、歩いたり車で移動したりして、後からどの距離でメッセージが届かなくなったかを分析できます。 +ノード間のリンク品質を評価するための、自動レンジテストツールです。有効にすると、ノードはカウンターを増やしながらテストメッセージを定期的に送信します。受信側のノードがこれらのメッセージを記録するため、歩いたり車で移動したりして、後からどの距離でメッセージが届かなくなったかを分析できます。 -| 設定項目 | 説明 | -| -------------------------------------------- | ----------------------------------------------------------------------------------------- | -| レンジテストを有効化 | レンジテストを有効化 | -| 送信者のメッセージ間隔 (秒) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| ストレージにCSVファイルを保存(ESP32のみ) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| 設定項目 | 説明 | +| -------------------------------------------- | ---------------------------------------------------------------------------------------- | +| レンジテストを有効化 | レンジテストを有効化 | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| ストレージにCSVファイルを保存(ESP32のみ) | Log received test data to the node's own filesystem. ESP32 hardware only | -### テレメトリモジュール +### Telemetry module -ノードがメッシュと共有するテレメトリデータを制御します。 テレメトリには、デバイスの状態(バッテリー、連続稼働時間)と環境センサーのデータ(温度、湿度、気圧)が含まれます。 +ノードがメッシュと共有するテレメトリデータを制御します。テレメトリには、デバイスの状態(バッテリー、連続稼働時間)と環境センサーのデータ(温度、湿度、気圧)が含まれます。 Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| 設定項目 | 説明 | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| デバイステレメトリを送信 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| デバイスメトリクスの更新間隔 | How often to report battery, uptime and channel utilization | -| 環境メトリクスモジュールを有効化 | Report the attached environment sensors | -| 環境メトリクスの更新間隔 | How often to report them | -| 環境メトリクスを画面上で有効化 | Also show these readings on the device's own display | -| 環境メトリクスは華氏を使用 | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| 空気品質測定モジュールを有効化 | Report particulate and CO₂ sensor data | -| 空気品質メトリクスの更新間隔 | How often to report them | -| 電源メトリクスモジュール有効 | Report the per-channel voltage and current readings | -| 電源メトリクスの更新間隔 | How often to report them | -| 電源メトリクスを画面上で有効化 | Also show power readings on the device's display | +| 設定項目 | 説明 | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| デバイステレメトリを送信 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| デバイスメトリクスの更新間隔 | How often to report battery, uptime and channel utilization | +| 環境メトリクスモジュールを有効化 | Report the attached environment sensors | +| 環境メトリクスの更新間隔 | How often to report them | +| 環境メトリクスを画面上で有効化 | Also show these readings on the device's own display | +| 環境メトリクスは華氏を使用 | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| 空気品質測定モジュールを有効化 | Report particulate and CO₂ sensor data | +| 空気品質メトリクスの更新間隔 | How often to report them | +| 電源メトリクスモジュール有効 | Report the per-channel voltage and current readings | +| 電源メトリクスの更新間隔 | How often to report them | +| 電源メトリクスを画面上で有効化 | Also show power readings on the device's display | 対応センサーと設定の推奨事項については、[テレメトリとセンサー](telemetry-and-sensors) を参照してください。 -### 定型メッセージモジュール +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). スマートフォンを接続していなくても送信できる、クイック送信メッセージのリストを定義します。フィールドでの使用に最適です。 +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). スマートフォンを接続していなくても送信できる、クイック送信メッセージのリストを定義します。フィールドでの使用に最適です。 | 設定項目 | 説明 | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,9 +165,9 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | 上下/選択入力を有効化 | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### オーディオモジュール +### Audio module -メッシュ上での低帯域幅の音声通信のための、Codec2 音声サポートです。 これは、Codec2 コーデックを使って音声を非常に小さなデータパケットにエンコードする、**実験的**な機能です。 +メッシュ上での低帯域幅の音声通信のための、Codec2 音声サポートです。これは、Codec2 コーデックを使って音声を非常に小さなデータパケットにエンコードする、**実験的**な機能です。 | 設定項目 | 説明 | | -------------- | ------------------------------------ | @@ -182,11 +181,11 @@ Pre-configured messages accessible from the radio's physical buttons (for radios > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). 音質は非常に低帯域です。「聞き取れる無線の声」程度で、電話並みの品質ではないと考えてください。 -### リモートハードウェアモジュール +### Remote Hardware module -メッシュネットワーク経由での GPIO 制御です。 リモートのノードが、別のノードの GPIO ピンを読み書きできるようにします。リレーの作動、スイッチの読み取り、離れた場所からの外部ハードウェア制御に便利です。 +メッシュネットワーク経由での GPIO 制御です。リモートのノードが、別のノードの GPIO ピンを読み書きできるようにします。リレーの作動、スイッチの読み取り、離れた場所からの外部ハードウェア制御に便利です。 -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | 設定項目 | 説明 | | --------------- | ------------------------------------- | @@ -194,21 +193,21 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | 未定義のピンへのアクセスを許可 | 任意の GPIO ピンへのアクセスを許可(セキュリティリスク) | | 使用可能な端子 | このノードがリモートの読み書き用に公開する GPIO ピン(最大 4 個) | -### 隣接ノード情報モジュール +### Neighbor Info module -直接受信した隣接ノードの情報をブロードキャストし、メッシュのトポロジーマッピングを可能にします。 有効にした各ノードは、受信できる他のノードとその信号品質のリストを定期的に共有します。 +直接受信した隣接ノードの情報をブロードキャストし、メッシュのトポロジーマッピングを可能にします。有効にした各ノードは、受信できる他のノードとその信号品質のリストを定期的に共有します。 -| 設定項目 | 説明 | -| --------------------------- | ------------------------------------------------------------------------------- | -| 近隣ノード情報を有効化 | 隣接ノードのブロードキャストを有効化 | -| 更新間隔 (秒) | 隣接ノードのリストをブロードキャストする頻度 | -| LoRaで送信 | MQTT/スマートフォンだけでなく、LoRa 経由でも隣接ノード情報をブロードキャストします。 デフォルトの鍵と名前を使用しているチャンネルでは利用できません | +| 設定項目 | 説明 | +| ----------- | ------------------------------------------------------------------------------ | +| 近隣ノード情報を有効化 | 隣接ノードのブロードキャストを有効化 | +| GPS ポーリング間隔 | 隣接ノードのリストをブロードキャストする頻度 | +| LoRaで送信 | MQTT/スマートフォンだけでなく、LoRa 経由でも隣接ノード情報をブロードキャストします。デフォルトの鍵と名前を使用しているチャンネルでは利用できません | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### アンビエントライティングモジュール +### Ambient Lighting module -対応ハードウェア上の、オンボードの NeoPixel やその他のアドレサブル RGB LED を制御します。 視覚的なステータス表示、通知ライト、装飾的な演出に使用できます。 +対応ハードウェア上の、オンボードの NeoPixel やその他のアドレサブル RGB LED を制御します。視覚的なステータス表示、通知ライト、装飾的な演出に使用できます。 | 設定項目 | 説明 | | ------ | ---------------- | @@ -216,49 +215,49 @@ See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topo | 電流 | LED の電流制限(0〜31) | | 赤/緑/青 | 各色チャンネルの値(0〜255) | -### 検知センサーモジュール +### Detection Sensor module ノードを、動きやドアを検知するセンサーアラートシステムに変えます。 GPIO ピンが状態の変化(動きの検知、ドアの開放)を検出すると、ノードはメッシュ経由でアラートメッセージをブロードキャストします。 -| 設定項目 | 説明 | -| ---------------------------------------- | ------------------------------------------- | -| 検出センサーを有効化 | 検知センサーを有効化 | -| モニターのGPIOピン | センサーに接続された GPIO ピン | -| 検出トリガーの種類 | ピンの状態を検知イベントにどう対応させるか(例:アクティブハイ/ロー、エッジトリガー) | -| INPUT_PULLUP モードを使用 | ピンの内蔵プルアップ抵抗を有効にする | -| 最小ブロードキャスト間隔(秒) | アラートのブロードキャスト間の最小時間 | -| 状態のブロードキャスト間隔 (秒) | 定期的な状態ブロードキャストの間隔 | -| アラートメッセージ付きのベルを送信 | アラートにベル文字を含める | -| 名前 | このセンサーのカスタム名 | +| 設定項目 | 説明 | +| ----------------------------------------- | ------------------------------------------- | +| 検出センサーを有効化 | 検知センサーを有効化 | +| モニターのGPIOピン | センサーに接続された GPIO ピン | +| 検出トリガーの種類 | ピンの状態を検知イベントにどう対応させるか(例:アクティブハイ/ロー、エッジトリガー) | +| INPUT_PULLUP モードを使用 | ピンの内蔵プルアップ抵抗を有効にする | +| Minimum time between detection broadcasts | アラートのブロードキャスト間の最小時間 | +| State Broadcast Interval | 定期的な状態ブロードキャストの間隔 | +| アラートメッセージ付きのベルを送信 | アラートにベル文字を含める | +| 名前 | このセンサーのカスタム名 | -### Paxcounter モジュール +### Paxcounter module People counter using Wi-Fi and BLE probe requests. スマートフォンやノートパソコンがネットワークを探すときに発するプローブ要求を受動的に受信して、近くのデバイスを数えます。 ESP32 デバイスでのみ利用できます。 -| 設定 | 説明 | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter を有効化 | 人数カウントを有効化 | -| 更新間隔 (秒) | カウントを報告する頻度 | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| 設定 | 説明 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter を有効化 | 人数カウントを有効化 | +| GPS ポーリング間隔 | カウントを報告する頻度 | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | -> 💡 **ヒント:** Paxcounter は、登山口やイベント会場などの人出を推定するのに便利です。 カウントはおおよその値です。1 人が複数のデバイスを持っていることがあります。 +> 💡 **ヒント:** Paxcounter は、登山口やイベント会場などの人出を推定するのに便利です。カウントはおおよその値です。1 人が複数のデバイスを持っていることがあります。 -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK モジュール +### TAK module ATAK および WinTAK と相互運用するための、Team Awareness Kit 連携です。 Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. 詳しい設定と使い方は、[TAK 連携](tak) を参照してください。 @@ -279,32 +278,33 @@ true before the entry appears in the module list: the radio runs firmware 2.8.0 **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| 操作 | 内容 | -| ----------- | ------------------------------------------------------------------------------------------------------ | -| 時刻を設定 | Sends your phone's clock to the radio | -| 再起動 | Restarts the radio | -| シャットダウン | Powers the radio down | -| 出荷時にリセット | Returns every setting to its factory default | -| NodeDBをリセット | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| 操作 | 内容 | +| ----------- | ---------------------------------------------------------------------------------------------- | +| 時刻を設定 | Sends your phone's clock to the node | +| 再起動 | Restarts the node | +| シャットダウン | Powers the node down | +| 出荷時にリセット | Returns every setting to its factory default | +| NodeDBをリセット | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### バックアップと復元 -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### 詳細設定 **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### ノードデータベースの整理 -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,22 +314,49 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. #### デバッグ -診断出力の表示・絞り込み・エクスポートを行う「**パケット**」タブと「**アプリログ**」タブを開きます。 詳しい手順は、[デバッグログ](debug-logs) を参照してください。 +診断出力の表示・絞り込み・エクスポートを行う「**パケット**」タブと「**アプリログ**」タブを開きます。詳しい手順は、[デバッグログ](debug-logs) を参照してください。 ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### 概要 @@ -352,14 +379,14 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### リモート管理のトラブルシューティング +### Troubleshooting remote admin -- **「対象ノードから応答がありません」**:対象が圏外、オフライン、または管理者鍵が一致していない可能性があります。 両方のノードで管理者鍵が一致しているか確認してください。 -- **変更が適用されない**:一部の設定は、反映に再起動が必要です。 保存後に「再起動」を実行してみてください。 -- **リモートの設定が表示されない**:自分のノードに、対象ノードの管理者鍵があることを確認してください。 管理チャンネルは、管理者鍵を設定すると自動的に構成されます。 +- **「対象ノードから応答がありません」**:対象が圏外、オフライン、または管理者鍵が一致していない可能性があります。両方のノードで管理者鍵が一致しているか確認してください。 +- **変更が適用されない**:一部の設定は、反映に再起動が必要です。保存後に「再起動」を実行してみてください。 +- **リモートの設定が表示されない**:自分のノードに、対象ノードの管理者鍵があることを確認してください。管理チャンネルは、管理者鍵を設定すると自動的に構成されます。 ## 関連トピック -- [設定:無線機とユーザー](settings-radio-user):基本の無線機とユーザープロファイルの設定 +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [モジュール設定リファレンス](https://meshtastic.org/docs/configuration/module):meshtastic.org にある詳細なモジュールのドキュメント - [FAQ](https://meshtastic.org/docs/faq/):meshtastic.org のよくある質問 diff --git a/docs/ja-rJP/user/settings-radio-user.md b/docs/ja-rJP/user/settings-radio-user.md index 5459434655..6706f385cf 100644 --- a/docs/ja-rJP/user/settings-radio-user.md +++ b/docs/ja-rJP/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: 設定:無線機とユーザー -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: 無線機のハードウェア、LoRa プリセット、ユーザープロファイル、位置共有、電源管理、セキュリティを設定します。 +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 設定 - radio-config @@ -13,14 +12,14 @@ aliases: # 設定:無線機とユーザー -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. 設定には、標準的な設定コントロール(ドロップダウン、トグル、スライダー)を使用します: @@ -36,20 +35,20 @@ only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Blu On **Settings → User**. -| 設定項目 | 説明 | -| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 正式名称 | 表示名(最大 39 文字) | -| 短縮名 | 4 文字の短縮名 | -| ステータスメッセージ | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| メッセージ不可 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| アマチュア無線従事者 (ハム/HAM) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| 設定項目 | 説明 | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 正式名称 | 表示名(最大 39 文字) | +| 短縮名 | 4 文字の短縮名 | +| ステータスメッセージ | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| メッセージ不可 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### 変更を適用する +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | 再ブロードキャストモード | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | ノード情報のブロードキャスト間隔 | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | ダブルタップをボタンとして使用 | Treat a double tap as a button press | 無効 | -| トリプルクリックでアドホック Ping | Send an ad-hoc position ping on a triple click | 無効 | +| トリプルクリックでアドホック Ping | Send an ad-hoc position ping on a triple click | 有効 | | LED ハートビート | Blink the status LED periodically | 有効 | | タイムゾーン | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,34 +74,41 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| 設定項目 | 説明 | デフォルト | -| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| リージョン | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | 未設定(要設定) | -| プリセット | 速度と距離のトレードオフ | LongFast | -| ホップ数 | 再送信の最大ホップ数 | 3 | -| 送信出力 | 送信出力(dBm)。0 = リージョンで許可された最大値 | 0(リージョン最大) | -| 周波数の上書き | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| プリセットを使用 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 拡散率 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| 符号化レート | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| 帯域 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| 周波数スロット | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| 送信を有効化 | Turning this off makes the node receive-only | On | -| デューティサイクルを上書き | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | オフ | -| MQTT を無視 | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| MQTT への送信を許可 | Allow your packets to be forwarded to MQTT by gateways | オフ | -| RX ブーストゲイン | Extra receive gain on SX126x radios; costs a little current | オフ | -| PAファン無効 | Turn off the power-amplifier fan on hardware that has one | オフ | +| 設定項目 | 説明 | デフォルト | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| リージョン | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | 未設定(要設定) | +| プリセット | 速度と距離のトレードオフ | LongFast | +| ホップ数 | 再送信の最大ホップ数 | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0(リージョン最大) | +| 周波数の上書き | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| プリセットを使用 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| 拡散率 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| 符号化レート | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| 帯域 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| 周波数スロット | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| 送信を有効化 | Turning this off makes the node receive-only | On | +| デューティサイクルを上書き | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | オフ | +| MQTT を無視 | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| MQTT への送信を許可 | Allow your packets to be forwarded to MQTT by gateways | オフ | +| RX ブーストゲイン | Extra receive gain on SX126x radios; costs a little current | オフ | +| PAファン無効 | Turn off the power-amplifier fan on hardware that has one | オフ | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. 詳しくは、meshtastic.org の [リージョン設定ガイド](https://meshtastic.org/docs/getting-started/initial-config) を参照してください。 -### モデムプリセット +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. -> 💡 **ヒント:** **SNR 限界**の値は意図的に負の数になっています。 LoRa はノイズフロアを_下回る_信号でも復調できるため、より負の大きい限界値ほど、そのプリセットは弱くノイズの多い信号に耐えられます(より遠くまで届きます)。 詳しい説明は、[信号メーターの仕組み](signal-meter) を参照してください。 +> 💡 **ヒント:** **SNR 限界**の値は意図的に負の数になっています。 LoRa はノイズフロアを_下回る_信号でも復調できるため、より負の大きい限界値ほど、そのプリセットは弱くノイズの多い信号に耐えられます(より遠くまで届きます)。詳しい説明は、[信号メーターの仕組み](signal-meter) を参照してください。 | プリセット | 距離 | 速度 | SNR 限界 | 最適な用途 | | ------------------ | ----------------------- | ------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -120,59 +126,57 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz 帯(62.5 kHz 帯域幅)。Long Fast に相当 | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **非推奨**:まだ選択できますが、将来のファームウェアリリースで削除される可能性があります | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **非推奨**:まだ選択できますが、将来のファームウェアリリースで削除される可能性があります | > ℹ️ **注意:** この表では、一般的な短い名前を使用しています。 The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### モデムプリセットを選ぶ +#### Choosing a modem preset モデムプリセットは、**距離**と**データ速度**の基本的なトレードオフを制御します: -- **遅いプリセット**は拡散をより多く使い、より弱い信号レベルでも復調できるようにします(SNR 限界が低い)。 これは、より遠くまで届く一方で、1 秒あたりのバイト数が少ないことを意味します。 +- **遅いプリセット**は拡散をより多く使い、より弱い信号レベルでも復調できるようにします(SNR 限界が低い)。これは、より遠くまで届く一方で、1 秒あたりのバイト数が少ないことを意味します。 - **速いプリセット**は 1 回の送信でより多くのデータを詰め込みますが、復調にはより強い信号が必要です。 **実用的な指針:** -- **都市部のメッシュ(多数のノード、短距離):** **Long Fast**(デフォルト)または **Short Fast** を使用します。 速度が速いほど、多くのノードがチャンネルを共有するときの電波利用時間の輻輳が減ります。 -- **地方・まばらなメッシュ(少数のノード、長距離):** **Long Moderate** を使用します。 ノードが離れている場合は、速度よりも距離が重要です。 +- **都市部のメッシュ(多数のノード、短距離):** **Long Fast**(デフォルト)または **Short Fast** を使用します。速度が速いほど、多くのノードがチャンネルを共有するときの電波利用時間の輻輳が減ります。 +- **地方・まばらなメッシュ(少数のノード、長距離):** **Long Moderate** を使用します。ノードが離れている場合は、速度よりも距離が重要です。 - **EU 866/868 MHz の規制対応:** **Lite Fast**、**Lite Slow**、**Narrow Fast**、**Narrow Slow** を使用します。これらは、より狭い帯域幅で EU の SRD/868 MHz 帯に最適化されています。 - **固定インフラのリンク:** 良好なアンテナと見通しがある専用のポイントツーポイントリンクには、**Short Turbo** または **Long Turbo** を使用します。 - **混在した環境:** **Long Fast** のままにします。これはコミュニティのデフォルトで、地域内の他のユーザーとの互換性を確保します。 -All nodes on the same channel must use the same modem preset. プリセットが一致しないノードは、同じ周波数と暗号化鍵を共有していても通信できません。 +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. -The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. 高所(丘の上、屋上)にあると、実効的な距離が大幅に伸びます。 適切に設置された Long Fast のルーターは、地上に置かれた Long Slow のノードを上回ることがよくあります。 +The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. 高所(丘の上、屋上)にあると、実効的な距離が大幅に伸びます。適切に設置された Long Fast のルーターは、地上に置かれた Long Slow のノードを上回ることがよくあります。 ### 表示設定 -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| 設定項目 | 説明 | -| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 画面オンの時間 | How long the display stays lit before sleeping | -| カルーセルの間隔 | How often the radio cycles between screens on its own | -| 表示モード | Screen layout/density used by the firmware | -| 表示単位 | Metric or Imperial on the radio's screen | -| 12時間の時計形式を使用 | Show the radio's clock as 12-hour rather than 24-hour | -| 太字の見出し | Draw the screen's heading text in bold | -| 画面反転 | Rotate the display 180° for an inverted mounting | -| OLED タイプ | 自動、SSD1306、SH1106、SH1107 | -| タップまたは動作で起動 | Light the screen when the radio is tapped or moved | -| コンパスの向き | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| 常に北を上にする | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| 設定項目 | 説明 | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 画面オンの時間 | How long the display stays lit before sleeping | +| カルーセルの間隔 | How often the node cycles between screens on its own | +| 表示モード | Screen layout/density used by the firmware | +| 表示単位 | Metric or Imperial on the node's screen | +| 12時間の時計形式を使用 | Show the node's clock as 12-hour rather than 24-hour | +| 太字の見出し | Draw the screen's heading text in bold | +| 画面反転 | Rotate the display 180° for an inverted mounting | +| OLED タイプ | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| タップまたは動作で起動 | Light the screen when the node is tapped or moved | +| コンパスの向き | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| 常に北を上にする | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### 位置情報設定 On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | 設定項目 | 説明 | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS モード(物理ハードウェア) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS ポーリング間隔 | How often the radio asks its GPS for a fix | +| GPS ポーリング間隔 | How often the node asks its GPS for a fix | | ブロードキャスト間隔 | How often the position is shared with the mesh | | スマート位置 | Broadcast based on movement rather than purely on the clock | | スマート間隔 | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | 設定項目 | 説明 | | ------------------------------------------- | --------------------------------------------------------------- | -| 省電力モードを有効化 | Let the radio sleep aggressively between activity | +| 省電力モードを有効化 | Let the node sleep aggressively between activity | | 電源喪失時にシャットダウン | Power the device down after external power disappears | | スーパーディープスリープの時間 | How long the deepest sleep state lasts | -| 最小起動時間 | The shortest time the radio stays awake once woken | +| 最小起動時間 | The shortest time the node stays awake once woken | | Bluetooth 待機時間 | How long to wait for a phone to connect before sleeping | | ADC倍率のオーバーライド | Turn on a manual correction for battery-voltage readings | | ADC倍率のオーバーライド比 | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### ネットワーク設定 -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | 設定項目 | 説明 | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | パスワード | ネットワークのパスワード | | イーサネット有効 | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth 設定 -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | 設定項目 | 説明 | | ------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | 設定項目 | 説明 | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 公開鍵 | ノードの公開鍵(読み取り専用) | | 管理者鍵 | Keys permitted to administer this node remotely — up to three | -| 秘密鍵 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| 秘密鍵 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | 秘密鍵を再生成 | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~管理チャンネルを有効化~~ | ⚠️ 削除されました:管理者キーを設定すると自動的に構成されるようになりました | | シリアルコンソール | Serial console over the Stream API | -| デバッグログAPIを有効化 | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| 管理モード | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| デバッグログAPIを有効化 | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| 管理モード | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | 鍵をバックアップ | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | 鍵を復元 | バックアップした鍵をノードに書き戻します(バックアップがある場合に利用可能) | | 鍵のバックアップを削除 | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ja-rJP/user/signal-meter.md b/docs/ja-rJP/user/signal-meter.md index a1c88df451..1f4d5c9a5b 100644 --- a/docs/ja-rJP/user/signal-meter.md +++ b/docs/ja-rJP/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: Meshtastic の信号メーターの仕組み -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: 信号メーターが、LoRa モデムプリセットに対する SNR から品質をどう評価するかを説明します。スペクトラム拡散、プリセット、バーが実際に意味するもの。 diff --git a/docs/ja-rJP/user/tak.md b/docs/ja-rJP/user/tak.md index 5420a75aa0..a56691f20e 100644 --- a/docs/ja-rJP/user/tak.md +++ b/docs/ja-rJP/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK 連携 -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: ATAK および WinTAK と相互運用します。CoT による位置共有、TAK の役割、プラグインの設定を説明します。 aliases: - tak @@ -75,12 +74,12 @@ Open **Settings → Advanced → TAK Server**. These are app settings stored on TAK 関連の役割を設定したノードは、標準のクライアントとは動作が異なります: -| 役割 | 説明 | -| --------------- | ----------------------------------------------------------------------------------------------------------- | -| **TAK** | 完全な TAK 相互運用。CoT データ、チャットメッセージ、PLI 更新を送受信します。 標準のクライアントとして機能し、さらに TAK ブリッジも兼ねます。 | -| **TAK Tracker** | 位置情報のみの TAK 出力。ユーザーの操作なしに、一定間隔で自動的に PLI をブロードキャストします。 無人の位置ビーコン(車両、機材、ウェイポイント)に最適化されています。 チャットメッセージは中継しません。 | +| 役割 | 説明 | +| --------------- | --------------------------------------------------------------------------------------------------------- | +| **TAK** | 完全な TAK 相互運用。CoT データ、チャットメッセージ、PLI 更新を送受信します。標準のクライアントとして機能し、さらに TAK ブリッジも兼ねます。 | +| **TAK Tracker** | 位置情報のみの TAK 出力。ユーザーの操作なしに、一定間隔で自動的に PLI をブロードキャストします。無人の位置ビーコン(車両、機材、ウェイポイント)に最適化されています。チャットメッセージは中継しません。 | -> 💡 **ヒント:** 位置の報告だけが必要なデバイス(例:車両に取り付けた無線機)には **TAK Tracker** を使用します。 ユーザーが TAK の運用に積極的に参加するデバイスには **TAK** を使用します。 +> 💡 **ヒント:** 位置の報告だけが必要なデバイス(例:車両に取り付けた無線機)には **TAK Tracker** を使用します。ユーザーが TAK の運用に積極的に参加するデバイスには **TAK** を使用します。 ### CoT(Cursor on Target)形式 @@ -90,10 +89,10 @@ TAK のメッセージは Cursor on Target の XML 形式を使用します。 Meshtastic は 2 つの TAK ワイヤ形式に対応しており、接続中の無線機のファームウェアに基づいて自動的に選択されます。手動での設定は必要ありません: -| 形式 | 互換性 | 機能 | -| -------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------- | -| V1(レガシー) | ファームウェア 2.7.x 以前 | ポート 72 での素の protobuf エンコード。 位置共有(PLI)とチャット(GeoChat)のみに対応。図形、マーカー、ルート、その他の型付き CoT イベントは破棄されます | -| V2(現行) | ファームウェア 2.8.0 以降 | ポート 78 でのコンパクトな zstd 圧縮エンコード。 V1 が対応するすべてに加えて、図形、マーカー、ルート、航空機、casevac、緊急、タスクの CoT タイプを追加します | +| 形式 | 互換性 | 機能 | +| -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| V1(レガシー) | ファームウェア 2.7.x 以前 | ポート 72 での素の protobuf エンコード。位置共有(PLI)とチャット(GeoChat)のみに対応。図形、マーカー、ルート、その他の型付き CoT イベントは破棄されます | +| V2(現行) | ファームウェア 2.8.0 以降 | ポート 78 でのコンパクトな zstd 圧縮エンコード。 V1 が対応するすべてに加えて、図形、マーカー、ルート、航空機、casevac、緊急、タスクの CoT タイプを追加します | ノードは V2 を実行していても、古いノードからのレガシーな V1 パケットを引き続き中継するため、ファームウェアが混在したメッシュでも動作し続けます。 @@ -105,6 +104,7 @@ Meshtastic は 2 つの TAK ワイヤ形式に対応しており、接続中の - チャットメッセージを、メッシュと TAK ネットワークの間で橋渡しできます - 位置の更新が、Meshtastic と TAK の間で双方向に流れます - TAK Tracker のノードは自動的に PLI をブロードキャストします。ATAK 側の設定なしで、その位置が ATAK のマップに表示されます +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ja-rJP/user/telemetry-and-sensors.md b/docs/ja-rJP/user/telemetry-and-sensors.md index a3eac20bee..82a62a3d71 100644 --- a/docs/ja-rJP/user/telemetry-and-sensors.md +++ b/docs/ja-rJP/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: テレメトリとセンサー -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: メッシュ上のセンサーデータ。対応する環境・大気質・電力センサーと、設定・表示のガイドを説明します。 aliases: - sensors @@ -43,11 +42,12 @@ Meshtastic のノードは、メッシュネットワーク全体でセンサー ### 大気質 -| センサー | メトリクス | 備考 | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | ガス抵抗/IAQ | 揮発性有機化合物 | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| センサー | メトリクス | 備考 | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | ガス抵抗/IAQ | 揮発性有機化合物 | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Meshtastic のノードは、メッシュネットワーク全体でセンサー Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### 光と UV | センサー | メトリクス | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| メトリクス | 単位 | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| 放射線 | µR/h | Card and chart | -| 重さ | kg or lb | Card only — load cells, such as a beehive scale | -| 距離 | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| メトリクス | 単位 | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| 放射線 | µR/h | Card and chart | +| 重さ | kg or lb | Card only — load cells, such as a beehive scale | +| 距離 | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## 電力メトリクス @@ -123,9 +126,9 @@ hand-tune them for mesh size. Lengthen them deliberately only to save battery. | PM10 | µg/m³ | 粗大粒子状物質 | | CO₂ | ppm | 二酸化炭素の濃度 | -SCD4x などの CO₂ センサーは、自身の温度と湿度も報告し、上記の測定値とともに表示されます。 アプリは、PM2.5 の履歴から **EPA NowCast AQI** の値も算出します。 +SCD4x などの CO₂ センサーは、自身の温度と湿度も報告し、上記の測定値とともに表示されます。アプリは、PM2.5 の履歴から **EPA NowCast AQI** の値も算出します。 -CO₂ の測定値は、深刻度に応じて色分けされます(良好 → 空気がこもる → 悪い → 危険 → 退避)。 正確な ppm の区分、色、AQI の詳細については、[ノードメトリクス:大気質](node-metrics#air-quality-metrics) を参照してください。 +CO₂ の測定値は、深刻度に応じて色分けされます(良好 → 空気がこもる → 悪い → 危険 → 退避)。正確な ppm の区分、色、AQI の詳細については、[ノードメトリクス:大気質](node-metrics#air-quality-metrics) を参照してください。 大気質データは、ノードの詳細画面で情報カードとして表示したり、時系列でグラフ化したり、CSV にエクスポートしたりできます。 @@ -139,9 +142,9 @@ CO₂ の測定値は、深刻度に応じて色分けされます(良好 → ## トラブルシューティング -- **環境データが表示されない?** リモートノードに物理センサーが接続されている必要があります(例:I2C の BME280)。 デバイステレメトリ(バッテリー、連続稼働時間)は常に利用できますが、環境メトリクスにはハードウェアが必要です。 -- **測定値が古い?** 報告間隔を確認してください。非常に長い間隔(7200 秒以上)では、データの更新頻度が低くなります。 リモートノードがまだオンラインであるかも確認してください。 -- **I2C バスでセンサーが競合している?** 一部のセンサーは I2C アドレスを共有しています。 同じバスに複数のセンサーがある場合は、無線機のシリアルデバッグ出力でアドレスの衝突がないか確認してください。 +- **環境データが表示されない?** リモートノードに物理センサーが接続されている必要があります(例:I2C の BME280)。デバイステレメトリ(バッテリー、連続稼働時間)は常に利用できますが、環境メトリクスにはハードウェアが必要です。 +- **測定値が古い?** 報告間隔を確認してください。非常に長い間隔(7200 秒以上)では、データの更新頻度が低くなります。リモートノードがまだオンラインであるかも確認してください。 +- **I2C バスでセンサーが競合している?** 一部のセンサーは I2C アドレスを共有しています。同じバスに複数のセンサーがある場合は、無線機のシリアルデバッグ出力でアドレスの衝突がないか確認してください。 ## 関連トピック diff --git a/docs/ja-rJP/user/translate.md b/docs/ja-rJP/user/translate.md index 15ba62c543..82d3e48360 100644 --- a/docs/ja-rJP/user/translate.md +++ b/docs/ja-rJP/user/translate.md @@ -1,6 +1,5 @@ --- title: アプリを翻訳する -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: アプリとそのドキュメントが Crowdin を通じてどう翻訳されるか、および翻訳に貢献するためのガイドラインを説明します。 @@ -28,10 +27,10 @@ The app and its in-app docs are translated on Crowdin — this page shows how to 1. **Crowdin プロジェクトにアクセスします。** [Meshtastic Android の Crowdin プロジェクト](https://crowdin.com/project/meshtastic-android) を開き、サインインするか、無料アカウントを作成します。 2. **言語を選びます。** 既存の言語を選択するか、[GitHub の issue](https://github.com/meshtastic/Meshtastic-Android/issues/new) を作成して新しい言語をリクエストします。 -3. **文字列を翻訳します。** Crowdin では、左側に英語の原文、右側に自分の翻訳が表示されます。 各文字列を翻訳して保存します。 +3. **文字列を翻訳します。** Crowdin では、左側に英語の原文、右側に自分の翻訳が表示されます。各文字列を翻訳して保存します。 4. **コンテキストを確認します。** 多くの文字列には、スクリーンショットやコンテキストのコメントが含まれています。これらを確認して、テキストがアプリのどこに表示されるかを把握してください。 A scheduled job pulls approved translations from Crowdin and opens a pull request; they ship once a maintainer merges it and a new build goes out. -> 💡 **ヒント:** 翻訳は短くしてください。 UI 文字列は、ボタン、チップ、狭い列に表示されることがよくあります。 翻訳が英語の原文よりも大幅に長くなる場合は、意味が明確なままになる範囲で短縮することを検討してください。 +> 💡 **ヒント:** 翻訳は短くしてください。 UI 文字列は、ボタン、チップ、狭い列に表示されることがよくあります。翻訳が英語の原文よりも大幅に長くなる場合は、意味が明確なままになる範囲で短縮することを検討してください。 ## 新しい言語を追加する @@ -80,8 +79,8 @@ A page that came from Crowdin is labeled **Community translated** under its titl ## 翻訳ガイドライン - 「LoRa」「MQTT」「BLE」「TAK」「SNR」「RSSI」などの技術用語は**翻訳しないでください**。これらは共通です。 -- **プレースホルダーはそのまま保ちます。** `%1$s` や `%d` のような文字列は、実行時に値が埋め込まれます。 自分の言語の文法上必要な場合を除き、削除したり並び替えたりしないでください。 -- **トーンを合わせます。** アプリは、親しみやすく率直な語り口を使っています。 過度に堅い言葉は避けてください。 +- **プレースホルダーはそのまま保ちます。** `%1$s` や `%d` のような文字列は、実行時に値が埋め込まれます。自分の言語の文法上必要な場合を除き、削除したり並び替えたりしないでください。 +- **トーンを合わせます。** アプリは、親しみやすく率直な語り口を使っています。過度に堅い言葉は避けてください。 - **Test if possible.** Switch your phone's language and open the app to see how translations look in context. ## 質問がありますか? diff --git a/docs/ja-rJP/user/units-and-locale.md b/docs/ja-rJP/user/units-and-locale.md index 853f6e5513..beb1772de4 100644 --- a/docs/ja-rJP/user/units-and-locale.md +++ b/docs/ja-rJP/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: 単位・計測・ロケール -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: アプリが、デバイスのロケールに基づいて温度・距離・速度などの計測値をどう表示するかを説明します。 @@ -18,9 +17,9 @@ The Meshtastic app automatically displays temperatures, distances, speeds, and t ## 仕組み -Meshtastic の無線機は、常に**メートル法の単位**(メートル、°C、m/s、hPa など)でデータを送信します。 アプリはこのデータを受信すると、デバイスのロケールで指定された単位系に変換して値を表示します。 +Meshtastic の無線機は、常に**メートル法の単位**(メートル、°C、m/s、hPa など)でデータを送信します。アプリはこのデータを受信すると、デバイスのロケールで指定された単位系に変換して値を表示します。 -Android では、計測の設定はシステムの**言語と地域**の設定によって決まります。 デスクトップ(JVM)では、アプリは JVM のデフォルトの `Locale` を使用します。 +Android では、計測の設定はシステムの**言語と地域**の設定によって決まります。デスクトップ(JVM)では、アプリは JVM のデフォルトの `Locale` を使用します。 Units follow your device's **region**, not the display language. Plain languages — like **English** in the app's own Language setting or Android's per-app language — keep the region your device is set to. A choice that names a region of its own, like **English (Canada)**, overrides it and brings that region's units with it. On Android 16+, the system-wide **Measurement system** preference overrides the region for distance, speed, and the other measurements — but not for temperature, which keeps following the region. diff --git a/docs/ja-rJP/user/widget.md b/docs/ja-rJP/user/widget.md index ff4f68b8a6..a2cdd0138a 100644 --- a/docs/ja-rJP/user/widget.md +++ b/docs/ja-rJP/user/widget.md @@ -1,6 +1,5 @@ --- title: ホーム画面ウィジェット -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Meshtastic のホーム画面ウィジェットを追加すると、アプリを開かずに、接続中の無線機のローカル統計をひと目で確認できます。 diff --git a/docs/ko-rKR/user/app-functions.md b/docs/ko-rKR/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ko-rKR/user/app-functions.md +++ b/docs/ko-rKR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ko-rKR/user/connections.md b/docs/ko-rKR/user/connections.md index 368fa6e138..3f6edf886a 100644 --- a/docs/ko-rKR/user/connections.md +++ b/docs/ko-rKR/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 블루투스 | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| 네트워크 | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ko-rKR/user/debug-logs.md b/docs/ko-rKR/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ko-rKR/user/debug-logs.md +++ b/docs/ko-rKR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ko-rKR/user/desktop.md b/docs/ko-rKR/user/desktop.md index fac8e481b1..b0eb85abc3 100644 --- a/docs/ko-rKR/user/desktop.md +++ b/docs/ko-rKR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ko-rKR/user/discovery.md b/docs/ko-rKR/user/discovery.md index f4299eaace..52fcf57a48 100644 --- a/docs/ko-rKR/user/discovery.md +++ b/docs/ko-rKR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | 설명 | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | 설명 | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### 이웃 정보 @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ko-rKR/user/firmware.md b/docs/ko-rKR/user/firmware.md index db16d55e5b..19b7059ec4 100644 --- a/docs/ko-rKR/user/firmware.md +++ b/docs/ko-rKR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ko-rKR/user/help-and-docs.md b/docs/ko-rKR/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ko-rKR/user/help-and-docs.md +++ b/docs/ko-rKR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ko-rKR/user/map-and-waypoints.md b/docs/ko-rKR/user/map-and-waypoints.md index d5e1bba45c..5862fc27a4 100644 --- a/docs/ko-rKR/user/map-and-waypoints.md +++ b/docs/ko-rKR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ko-rKR/user/messages-and-channels.md b/docs/ko-rKR/user/messages-and-channels.md index 53da3f7c92..0cb871b50d 100644 --- a/docs/ko-rKR/user/messages-and-channels.md +++ b/docs/ko-rKR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ko-rKR/user/mqtt.md b/docs/ko-rKR/user/mqtt.md index 23e8471fd3..6c9c4668a2 100644 --- a/docs/ko-rKR/user/mqtt.md +++ b/docs/ko-rKR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ko-rKR/user/node-metrics.md b/docs/ko-rKR/user/node-metrics.md index 215f871d42..2b556b3056 100644 --- a/docs/ko-rKR/user/node-metrics.md +++ b/docs/ko-rKR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ko-rKR/user/nodes.md b/docs/ko-rKR/user/nodes.md index 7fb1f2eac7..ad0dd73b53 100644 --- a/docs/ko-rKR/user/nodes.md +++ b/docs/ko-rKR/user/nodes.md @@ -1,8 +1,7 @@ --- title: 노드 -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| 필터 | 설명 | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 필터 | 설명 | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | 설명 | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | 최근 수신 | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | 거리 | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ko-rKR/user/notifications.md b/docs/ko-rKR/user/notifications.md new file mode 100644 index 0000000000..361a5d3539 --- /dev/null +++ b/docs/ko-rKR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----- | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| 메시지기기 | DM 알림 | A message sent directly to you | The conversation | +| 메시지기기 | 메시지 발송 알림 | A message on one of your channels | The channel | +| 메시지기기 | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| 메시지기기 | 경고 알림 | A critical alert from a node | The conversation | +| Mesh | 새로운 노드 알림 | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | 배터리 부족 알림 (즐겨찾기 노드) | A favorite node's battery running low | The node's details | +| 장치 | 서비스 알림 | The connection to your node while the app runs in the background | The app | +| 장치 | 배터리 부족 알림 | Your node's battery running low | The node's details | +| 장치 | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| 장치 | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ko-rKR/user/onboarding.md b/docs/ko-rKR/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/ko-rKR/user/onboarding.md +++ b/docs/ko-rKR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ko-rKR/user/settings-module-admin.md b/docs/ko-rKR/user/settings-module-admin.md index 51d8af2089..e9585b9c76 100644 --- a/docs/ko-rKR/user/settings-module-admin.md +++ b/docs/ko-rKR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## 모듈 설정 Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | 설명 | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT 활성화 | Toggle MQTT bridge | -| 서버 주소 | MQTT broker address | -| 사용자명 | Authentication username | -| 비밀번호 | Authentication password | -| 암호화 사용 | Encrypt MQTT payloads | -| JSON 사용 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS 사용 | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client 사용 | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| 맵 보고 | Publish position to the public map — see the Map reporting group that follows | +| Setting | 설명 | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT 활성화 | Toggle MQTT bridge | +| 서버 주소 | MQTT broker address | +| 사용자명 | Authentication username | +| 비밀번호 | Authentication password | +| 암호화 사용 | Encrypt MQTT payloads | +| JSON 사용 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS 사용 | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client 사용 | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| 맵 보고 | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | 설명 | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 동의합니다. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| 맵 보고 간격 (초) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | 설명 | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 동의합니다. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +75,9 @@ Enables serial port communication for external device integrations (GPS modules, | 시간 초과 | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,11 +93,11 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | 진동 출력 (GPIO) | Pin the vibration motor is wired to | | PWM 부저 사용 | Drive the buzzer with PWM, which allows tones rather than a single pitch | | I2S 부저 사용 | Send the alert through an I2S audio output instead | -| 출력 지속시간 (밀리초) | How long a single alert lasts | -| 반복 종료 시간 (초) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | 벨소리 | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | 설명 | -| --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| 거리 테스트 활성화 | Activate range testing | -| 송신 장치 메시지 간격 (초) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| .CSV 파일 저장 (EPS32만 동작) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | 설명 | +| --------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| 거리 테스트 활성화 | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| .CSV 파일 저장 (EPS32만 동작) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | 설명 | -| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| 환경 메트릭 모듈 사용 | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| 환경 메트릭 화면 사용 | Also show these readings on the device's own display | -| 환경 메트릭에서 화씨 사용 | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| 대기질 메트릭 모듈 사용 | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| 전력 메트릭 모듈 사용 | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| 전력 메트릭 화면 사용 | Also show power readings on the device's display | +| Setting | 설명 | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| 환경 메트릭 모듈 사용 | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| 환경 메트릭 화면 사용 | Also show these readings on the device's own display | +| 환경 메트릭에서 화씨 사용 | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| 대기질 메트릭 모듈 사용 | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| 전력 메트릭 모듈 사용 | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| 전력 메트릭 화면 사용 | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | 설명 | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | 업/다운/선택 입력 활성화 | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | 설명 | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | 설명 | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| 이웃 정보 활성화 | Activate neighbor broadcasting | -| 업데이트 간격 (초) | How often to broadcast neighbor list | -| LoRa로 전송 | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | 설명 | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| 이웃 정보 활성화 | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| LoRa로 전송 | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | 전류 | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | 설명 | -| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| 감지 센서 활성화 | Activate detection sensor | -| 상태 모니터링 GPIO 핀 | GPIO pin connected to sensor | -| 디텍션 트리거 타입 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| INPUT_PULLUP 모드 사용 | Enable the pin's internal pull-up resistor | -| 최소 전송 간격 (초) | Minimum time between alert broadcasts | -| 상태 전송 간격 (초) | Periodic state broadcast interval | -| 알람 메시지와 벨 전송 | Include bell character in alerts | -| 식별 이름 | Custom name for this sensor | +| Setting | 설명 | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| 감지 센서 활성화 | Activate detection sensor | +| 상태 모니터링 GPIO 핀 | GPIO pin connected to sensor | +| 디텍션 트리거 타입 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| INPUT_PULLUP 모드 사용 | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| 알람 메시지와 벨 전송 | Include bell character in alerts | +| 식별 이름 | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | 설명 | -| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| 팍스카운터 활성화 | Activate people counting | -| 업데이트 간격 (초) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | 설명 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| 팍스카운터 활성화 | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| 재부팅 | Restarts the radio | -| 종료 | Powers the radio down | -| 공장초기화 | Returns every setting to its factory default | -| 노드 목록 리셋 | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| 재부팅 | Restarts the node | +| 종료 | Powers the node down | +| 공장초기화 | Returns every setting to its factory default | +| 노드 목록 리셋 | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### 백업 & 복구 -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### 고급 **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### 앱에 대하여 @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ko-rKR/user/settings-radio-user.md b/docs/ko-rKR/user/settings-radio-user.md index ba9fb08496..c061f518df 100644 --- a/docs/ko-rKR/user/settings-radio-user.md +++ b/docs/ko-rKR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 설정 - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | 설명 | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 긴 이름 | Your display name (up to 39 characters) | -| 짧은 이름 | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| 메시지 제한 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | 설명 | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 긴 이름 | Your display name (up to 39 characters) | +| 짧은 이름 | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| 메시지 제한 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | 중계 모드 | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | 노드 정보 발송 주기 | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | 활성화 | | LED Heartbeat | Blink the status LED periodically | 활성화 | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | 설명 | 기본값 | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 지역 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| 전송 출력 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| 프리셋 사용 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| 대역폭 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| 주파수 슬롯 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| 전송 활성화 | Turning this off makes the node receive-only | On | -| Duty Cycle 무시 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| MQTT로 부터 수신 무시 | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan 비활성화됨 | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | 설명 | 기본값 | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| 지역 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| 프리셋 사용 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| 대역폭 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| 주파수 슬롯 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| 전송 활성화 | Turning this off makes the node receive-only | On | +| Duty Cycle 무시 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| MQTT로 부터 수신 무시 | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan 비활성화됨 | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### 화면 설정 -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | 설명 | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| 디스플레이 모드 | Screen layout/density used by the firmware | -| 단위 표시 | Metric or Imperial on the radio's screen | -| 12시간제 보기 | Show the radio's clock as 12-hour rather than 24-hour | -| 상태표시줄 볼드체 | Draw the screen's heading text in bold | -| 화면 뒤집기 | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| 나침반 방향 | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | 설명 | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| 디스플레이 모드 | Screen layout/density used by the firmware | +| 단위 표시 | Metric or Imperial on the node's screen | +| 12시간제 보기 | Show the node's clock as 12-hour rather than 24-hour | +| 상태표시줄 볼드체 | Draw the screen's heading text in bold | +| 화면 뒤집기 | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| 나침반 방향 | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### 위치 설정 On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | 설명 | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | 전송 간격 | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | 설명 | | ------------------------------------------------ | --------------------------------------------------------------- | -| 저전력 모드 설정 | Let the radio sleep aggressively between activity | +| 저전력 모드 설정 | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### 네트워크 설정 -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | 설명 | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | 비밀번호 | 네트워크 암호 | | 이더넷 활성화 | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### 블루투스 설정 -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | 설명 | | -------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | 설명 | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 공개 키 | Your node's public key (read-only) | | Admin 키 | Keys permitted to administer this node remotely — up to three | -| 개인 키 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| 개인 키 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | 개인 키 다시 생성하기 | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | 시리얼 콘솔 | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| 관리 모드 | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| 관리 모드 | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ko-rKR/user/signal-meter.md b/docs/ko-rKR/user/signal-meter.md index 8a72c88336..416885c85a 100644 --- a/docs/ko-rKR/user/signal-meter.md +++ b/docs/ko-rKR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ko-rKR/user/tak.md b/docs/ko-rKR/user/tak.md index e487764b60..155c67af50 100644 --- a/docs/ko-rKR/user/tak.md +++ b/docs/ko-rKR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ko-rKR/user/telemetry-and-sensors.md b/docs/ko-rKR/user/telemetry-and-sensors.md index b8ca8a0201..13886ea823 100644 --- a/docs/ko-rKR/user/telemetry-and-sensors.md +++ b/docs/ko-rKR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| 복사 | µR/h | Card and chart | -| 무게 | kg or lb | Card only — load cells, such as a beehive scale | -| 거리 | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| 복사 | µR/h | Card and chart | +| 무게 | kg or lb | Card only — load cells, such as a beehive scale | +| 거리 | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/ko-rKR/user/translate.md b/docs/ko-rKR/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/ko-rKR/user/translate.md +++ b/docs/ko-rKR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ko-rKR/user/units-and-locale.md b/docs/ko-rKR/user/units-and-locale.md index 55d28b4ea2..1365e4653e 100644 --- a/docs/ko-rKR/user/units-and-locale.md +++ b/docs/ko-rKR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ko-rKR/user/widget.md b/docs/ko-rKR/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ko-rKR/user/widget.md +++ b/docs/ko-rKR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/lt-rLT/user/app-functions.md b/docs/lt-rLT/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/lt-rLT/user/app-functions.md +++ b/docs/lt-rLT/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/lt-rLT/user/connections.md b/docs/lt-rLT/user/connections.md index 010e0059d1..760dd39893 100644 --- a/docs/lt-rLT/user/connections.md +++ b/docs/lt-rLT/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/lt-rLT/user/debug-logs.md b/docs/lt-rLT/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/lt-rLT/user/debug-logs.md +++ b/docs/lt-rLT/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/lt-rLT/user/desktop.md b/docs/lt-rLT/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/lt-rLT/user/desktop.md +++ b/docs/lt-rLT/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/lt-rLT/user/discovery.md b/docs/lt-rLT/user/discovery.md index af608e5887..65fb2db11f 100644 --- a/docs/lt-rLT/user/discovery.md +++ b/docs/lt-rLT/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Aprašymas | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Aprašymas | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/lt-rLT/user/firmware.md b/docs/lt-rLT/user/firmware.md index 15e477d350..2be34f1fff 100644 --- a/docs/lt-rLT/user/firmware.md +++ b/docs/lt-rLT/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/lt-rLT/user/help-and-docs.md b/docs/lt-rLT/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/lt-rLT/user/help-and-docs.md +++ b/docs/lt-rLT/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/lt-rLT/user/map-and-waypoints.md b/docs/lt-rLT/user/map-and-waypoints.md index 6b7b097759..c620e58e8d 100644 --- a/docs/lt-rLT/user/map-and-waypoints.md +++ b/docs/lt-rLT/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/lt-rLT/user/messages-and-channels.md b/docs/lt-rLT/user/messages-and-channels.md index eee3d40664..45769b0010 100644 --- a/docs/lt-rLT/user/messages-and-channels.md +++ b/docs/lt-rLT/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/lt-rLT/user/mqtt.md b/docs/lt-rLT/user/mqtt.md index 295c576617..37490bd324 100644 --- a/docs/lt-rLT/user/mqtt.md +++ b/docs/lt-rLT/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/lt-rLT/user/node-metrics.md b/docs/lt-rLT/user/node-metrics.md index 70519b1a76..07067c570d 100644 --- a/docs/lt-rLT/user/node-metrics.md +++ b/docs/lt-rLT/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/lt-rLT/user/nodes.md b/docs/lt-rLT/user/nodes.md index f44b7ad793..7fbdfbc6da 100644 --- a/docs/lt-rLT/user/nodes.md +++ b/docs/lt-rLT/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtras | Aprašymas | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtras | Aprašymas | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Aprašymas | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Seniausiai girdėtas | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Atstumas | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/lt-rLT/user/notifications.md b/docs/lt-rLT/user/notifications.md new file mode 100644 index 0000000000..ea370716cd --- /dev/null +++ b/docs/lt-rLT/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Naujo įtaiso pranešimas | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Paslaugos pranešimai | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/lt-rLT/user/onboarding.md b/docs/lt-rLT/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/lt-rLT/user/onboarding.md +++ b/docs/lt-rLT/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/lt-rLT/user/settings-module-admin.md b/docs/lt-rLT/user/settings-module-admin.md index ca3024b619..1b7b1aa43c 100644 --- a/docs/lt-rLT/user/settings-module-admin.md +++ b/docs/lt-rLT/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Modulio konfigūracija Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Aprašymas | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Aprašymas | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Aprašymas | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Aprašymas | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Baigėsi laikas | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Aprašymas | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Aprašymas | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Aprašymas | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Aprašymas | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Aprašymas | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Aprašymas | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Aprašymas | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Aprašymas | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Aprašymas | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Aprašymas | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Aprašymas | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Aprašymas | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Aprašymas | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Aprašymas | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| --------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Perkrauti | Restarts the radio | -| Išjungti | Powers the radio down | -| Gamyklinis atstatymas | Returns every setting to its factory default | -| NodeDB perkrauti | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| --------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Perkrauti | Restarts the node | +| Išjungti | Powers the node down | +| Gamyklinis atstatymas | Returns every setting to its factory default | +| NodeDB perkrauti | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Apie @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/lt-rLT/user/settings-radio-user.md b/docs/lt-rLT/user/settings-radio-user.md index bfe637382c..d5161ea2c3 100644 --- a/docs/lt-rLT/user/settings-radio-user.md +++ b/docs/lt-rLT/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Aprašymas | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Aprašymas | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Aprašymas | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regionas | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Aprašymas | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regionas | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Aprašymas | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Aprašymas | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Aprašymas | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Aprašymas | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Aprašymas | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Aprašymas | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Aprašymas | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Viešasis raktas | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Privatus raktas | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privatus raktas | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/lt-rLT/user/signal-meter.md b/docs/lt-rLT/user/signal-meter.md index 6dde721fe7..bbca466347 100644 --- a/docs/lt-rLT/user/signal-meter.md +++ b/docs/lt-rLT/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/lt-rLT/user/tak.md b/docs/lt-rLT/user/tak.md index ddffe9e8e1..ee8f61c0d9 100644 --- a/docs/lt-rLT/user/tak.md +++ b/docs/lt-rLT/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/lt-rLT/user/telemetry-and-sensors.md b/docs/lt-rLT/user/telemetry-and-sensors.md index 832e93167e..cc45da141b 100644 --- a/docs/lt-rLT/user/telemetry-and-sensors.md +++ b/docs/lt-rLT/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Atstumas | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Atstumas | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/lt-rLT/user/translate.md b/docs/lt-rLT/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/lt-rLT/user/translate.md +++ b/docs/lt-rLT/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/lt-rLT/user/units-and-locale.md b/docs/lt-rLT/user/units-and-locale.md index 7963b15c03..32d7f668cd 100644 --- a/docs/lt-rLT/user/units-and-locale.md +++ b/docs/lt-rLT/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/lt-rLT/user/widget.md b/docs/lt-rLT/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/lt-rLT/user/widget.md +++ b/docs/lt-rLT/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/nl-rNL/user/app-functions.md b/docs/nl-rNL/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/nl-rNL/user/app-functions.md +++ b/docs/nl-rNL/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/nl-rNL/user/connections.md b/docs/nl-rNL/user/connections.md index a8ed01d4d1..57eae30af4 100644 --- a/docs/nl-rNL/user/connections.md +++ b/docs/nl-rNL/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Netwerk | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/nl-rNL/user/debug-logs.md b/docs/nl-rNL/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/nl-rNL/user/debug-logs.md +++ b/docs/nl-rNL/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/nl-rNL/user/desktop.md b/docs/nl-rNL/user/desktop.md index 4e5c65b18b..cc003d849e 100644 --- a/docs/nl-rNL/user/desktop.md +++ b/docs/nl-rNL/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/nl-rNL/user/discovery.md b/docs/nl-rNL/user/discovery.md index 7de0058310..68d335590d 100644 --- a/docs/nl-rNL/user/discovery.md +++ b/docs/nl-rNL/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Beschrijving | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Beschrijving | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/nl-rNL/user/firmware.md b/docs/nl-rNL/user/firmware.md index 4a5dd58f59..c68f3f04ae 100644 --- a/docs/nl-rNL/user/firmware.md +++ b/docs/nl-rNL/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/nl-rNL/user/help-and-docs.md b/docs/nl-rNL/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/nl-rNL/user/help-and-docs.md +++ b/docs/nl-rNL/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/nl-rNL/user/map-and-waypoints.md b/docs/nl-rNL/user/map-and-waypoints.md index ee915c1dcf..0b45d7ac47 100644 --- a/docs/nl-rNL/user/map-and-waypoints.md +++ b/docs/nl-rNL/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/nl-rNL/user/messages-and-channels.md b/docs/nl-rNL/user/messages-and-channels.md index 8ef5dbcdf3..c88c51b32f 100644 --- a/docs/nl-rNL/user/messages-and-channels.md +++ b/docs/nl-rNL/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/nl-rNL/user/mqtt.md b/docs/nl-rNL/user/mqtt.md index cff6d07649..04fc556040 100644 --- a/docs/nl-rNL/user/mqtt.md +++ b/docs/nl-rNL/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/nl-rNL/user/node-metrics.md b/docs/nl-rNL/user/node-metrics.md index 42f01ff526..bedf1be6a4 100644 --- a/docs/nl-rNL/user/node-metrics.md +++ b/docs/nl-rNL/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/nl-rNL/user/nodes.md b/docs/nl-rNL/user/nodes.md index be088a9ade..3bcb8d2fe7 100644 --- a/docs/nl-rNL/user/nodes.md +++ b/docs/nl-rNL/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Beschrijving | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beschrijving | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Beschrijving | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Laatst gehoord | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Afstand | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/nl-rNL/user/notifications.md b/docs/nl-rNL/user/notifications.md new file mode 100644 index 0000000000..e9b3521eaf --- /dev/null +++ b/docs/nl-rNL/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| --------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Berichten | Direct message notifications | A message sent directly to you | The conversation | +| Berichten | Broadcast message notifications | A message on one of your channels | The channel | +| Berichten | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Berichten | Waarschuwingsmeldingen | A critical alert from a node | The conversation | +| Mesh | Nieuwe node meldingen | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Apparaat | Servicemeldingen | The connection to your node while the app runs in the background | The app | +| Apparaat | Batterij bijna leeg | Your node's battery running low | The node's details | +| Apparaat | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Apparaat | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/nl-rNL/user/onboarding.md b/docs/nl-rNL/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/nl-rNL/user/onboarding.md +++ b/docs/nl-rNL/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/nl-rNL/user/settings-module-admin.md b/docs/nl-rNL/user/settings-module-admin.md index 41b6b478c7..bc7929add5 100644 --- a/docs/nl-rNL/user/settings-module-admin.md +++ b/docs/nl-rNL/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Module-configuratie Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Beschrijving | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON uitvoer ingeschakeld | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS ingeschakeld | Use secure connection | | Root topic | Base MQTT topic path | -| Proxy to client ingeschakeld | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy to client ingeschakeld | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Kaartrapportage | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Beschrijving | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Beschrijving | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Time-Out | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Beschrijving | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Gebruik PWM zoemer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duur (milliseconden) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Beltoon | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Beschrijving | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Gebruik PWM zoemer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Beltoon | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Beschrijving | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Beschrijving | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Beschrijving | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Beschrijving | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Beschrijving | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Beschrijving | | ----------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Beschikbare pinnen | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Beschrijving | -| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update-interval (seconden) | How often to broadcast neighbor list | -| Zend over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Beschrijving | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Zend over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Huidige | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Beschrijving | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| Bewegingssensor Ingeschakeld | Activate detection sensor | -| GPIO pin om te monitoren | GPIO pin connected to sensor | -| Detectie trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimale broadcast (seconden) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Weergavenaam | Custom name for this sensor | +| Setting | Beschrijving | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Bewegingssensor Ingeschakeld | Activate detection sensor | +| GPIO pin om te monitoren | GPIO pin connected to sensor | +| Detectie trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Weergavenaam | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Beschrijving | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter ingeschakeld | Activate people counting | -| Update-interval (seconden) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Beschrijving | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter ingeschakeld | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Herstart | Restarts the radio | -| Zet uit | Powers the radio down | -| Reset naar fabrieksinstellingen | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Herstart | Restarts the node | +| Zet uit | Powers the node down | +| Reset naar fabrieksinstellingen | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Back-up & Herstellen -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Over @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/nl-rNL/user/settings-radio-user.md b/docs/nl-rNL/user/settings-radio-user.md index ca2a8aa1d1..594a8d3ec4 100644 --- a/docs/nl-rNL/user/settings-radio-user.md +++ b/docs/nl-rNL/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - instellingen - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Beschrijving | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Niet berichtbaar | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Beschrijving | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Niet berichtbaar | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Beschrijving | Standaard | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regio | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandbreedte | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Overschrijf Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Negeer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Beschrijving | Standaard | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regio | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandbreedte | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Overschrijf Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Negeer MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Weergave Configuratie -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Beschrijving | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Weergavemodus | Screen layout/density used by the firmware | -| Geef eenheden weer | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Scherm omdraaien | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Kompas oriëntatie | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Beschrijving | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Weergavemodus | Screen layout/density used by the firmware | +| Geef eenheden weer | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Scherm omdraaien | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Kompas oriëntatie | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Positie Configuratie On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beschrijving | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Beschrijving | | ------------------------------------------------ | --------------------------------------------------------------- | -| Energiebesparingsmodus inschakelen | Let the radio sleep aggressively between activity | +| Energiebesparingsmodus inschakelen | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Netwerkconfiguratie -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Beschrijving | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Wachtwoord | Network password | | Ethernet ingeschakeld | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Configuratie -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Beschrijving | | ---------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beschrijving | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Publieke sleutel | Your node's public key (read-only) | | Admin Sleutel | Keys permitted to administer this node remotely — up to three | -| Privésleutel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privésleutel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Seriële console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Beheerde modus | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Beheerde modus | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/nl-rNL/user/signal-meter.md b/docs/nl-rNL/user/signal-meter.md index a5cddebcea..31ad7afc10 100644 --- a/docs/nl-rNL/user/signal-meter.md +++ b/docs/nl-rNL/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/nl-rNL/user/tak.md b/docs/nl-rNL/user/tak.md index a18d0978d3..d8f0abd10b 100644 --- a/docs/nl-rNL/user/tak.md +++ b/docs/nl-rNL/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/nl-rNL/user/telemetry-and-sensors.md b/docs/nl-rNL/user/telemetry-and-sensors.md index e20cdec2eb..aeb5ef69e2 100644 --- a/docs/nl-rNL/user/telemetry-and-sensors.md +++ b/docs/nl-rNL/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Straling | µR/h | Card and chart | -| Gewicht | kg or lb | Card only — load cells, such as a beehive scale | -| Afstand | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Straling | µR/h | Card and chart | +| Gewicht | kg or lb | Card only — load cells, such as a beehive scale | +| Afstand | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/nl-rNL/user/translate.md b/docs/nl-rNL/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/nl-rNL/user/translate.md +++ b/docs/nl-rNL/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/nl-rNL/user/units-and-locale.md b/docs/nl-rNL/user/units-and-locale.md index 684961a860..9336b529c6 100644 --- a/docs/nl-rNL/user/units-and-locale.md +++ b/docs/nl-rNL/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/nl-rNL/user/widget.md b/docs/nl-rNL/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/nl-rNL/user/widget.md +++ b/docs/nl-rNL/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/no-rNO/user/app-functions.md b/docs/no-rNO/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/no-rNO/user/app-functions.md +++ b/docs/no-rNO/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/no-rNO/user/connections.md b/docs/no-rNO/user/connections.md index 9b9d96f828..587b58242b 100644 --- a/docs/no-rNO/user/connections.md +++ b/docs/no-rNO/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/no-rNO/user/debug-logs.md b/docs/no-rNO/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/no-rNO/user/debug-logs.md +++ b/docs/no-rNO/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/no-rNO/user/desktop.md b/docs/no-rNO/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/no-rNO/user/desktop.md +++ b/docs/no-rNO/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/no-rNO/user/discovery.md b/docs/no-rNO/user/discovery.md index dbf69b1171..e447c49f63 100644 --- a/docs/no-rNO/user/discovery.md +++ b/docs/no-rNO/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Beskrivelse | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Beskrivelse | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/no-rNO/user/firmware.md b/docs/no-rNO/user/firmware.md index 8f2cf69aa2..eddc3c029b 100644 --- a/docs/no-rNO/user/firmware.md +++ b/docs/no-rNO/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/no-rNO/user/help-and-docs.md b/docs/no-rNO/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/no-rNO/user/help-and-docs.md +++ b/docs/no-rNO/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/no-rNO/user/map-and-waypoints.md b/docs/no-rNO/user/map-and-waypoints.md index 341ad15461..fca4f000d1 100644 --- a/docs/no-rNO/user/map-and-waypoints.md +++ b/docs/no-rNO/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/no-rNO/user/messages-and-channels.md b/docs/no-rNO/user/messages-and-channels.md index 765338e8f1..b5f90ce2e7 100644 --- a/docs/no-rNO/user/messages-and-channels.md +++ b/docs/no-rNO/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/no-rNO/user/mqtt.md b/docs/no-rNO/user/mqtt.md index 302ebffd71..304e87031d 100644 --- a/docs/no-rNO/user/mqtt.md +++ b/docs/no-rNO/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/no-rNO/user/node-metrics.md b/docs/no-rNO/user/node-metrics.md index df41b9c05e..31961d7ed7 100644 --- a/docs/no-rNO/user/node-metrics.md +++ b/docs/no-rNO/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/no-rNO/user/nodes.md b/docs/no-rNO/user/nodes.md index 578a6ad356..5e7fb5e3e2 100644 --- a/docs/no-rNO/user/nodes.md +++ b/docs/no-rNO/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Beskrivelse | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beskrivelse | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Beskrivelse | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Sist hørt | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distanse | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/no-rNO/user/notifications.md b/docs/no-rNO/user/notifications.md new file mode 100644 index 0000000000..fce9078b1c --- /dev/null +++ b/docs/no-rNO/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Varsel om nye noder | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Tjeneste meldinger | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/no-rNO/user/onboarding.md b/docs/no-rNO/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/no-rNO/user/onboarding.md +++ b/docs/no-rNO/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/no-rNO/user/settings-module-admin.md b/docs/no-rNO/user/settings-module-admin.md index c971adfbc3..a4172d3bb7 100644 --- a/docs/no-rNO/user/settings-module-admin.md +++ b/docs/no-rNO/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Modul konfigurasjon Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Beskrivelse | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Beskrivelse | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Beskrivelse | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Beskrivelse | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Tidsavbrudd | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Beskrivelse | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Beskrivelse | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Beskrivelse | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Beskrivelse | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Beskrivelse | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Beskrivelse | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Beskrivelse | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Beskrivelse | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Beskrivelse | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Beskrivelse | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Beskrivelse | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Beskrivelse | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Beskrivelse | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Beskrivelse | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Omstart | Restarts the radio | -| Avslutt | Powers the radio down | -| Tilbakestill til fabrikkstandard | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Omstart | Restarts the node | +| Avslutt | Powers the node down | +| Tilbakestill til fabrikkstandard | Returns every setting to its factory default | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Om @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/no-rNO/user/settings-radio-user.md b/docs/no-rNO/user/settings-radio-user.md index eb5570f8cb..f77a9498d6 100644 --- a/docs/no-rNO/user/settings-radio-user.md +++ b/docs/no-rNO/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Beskrivelse | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Beskrivelse | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Beskrivelse | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Beskrivelse | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Beskrivelse | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Beskrivelse | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beskrivelse | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Beskrivelse | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Beskrivelse | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Beskrivelse | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beskrivelse | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Offentlig nøkkel | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Privat nøkkel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privat nøkkel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/no-rNO/user/signal-meter.md b/docs/no-rNO/user/signal-meter.md index 903f8c26d7..0e7f4e2040 100644 --- a/docs/no-rNO/user/signal-meter.md +++ b/docs/no-rNO/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/no-rNO/user/tak.md b/docs/no-rNO/user/tak.md index de6083ec53..a6c736f508 100644 --- a/docs/no-rNO/user/tak.md +++ b/docs/no-rNO/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/no-rNO/user/telemetry-and-sensors.md b/docs/no-rNO/user/telemetry-and-sensors.md index 6928b09780..d0f6c871cd 100644 --- a/docs/no-rNO/user/telemetry-and-sensors.md +++ b/docs/no-rNO/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distanse | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distanse | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/no-rNO/user/translate.md b/docs/no-rNO/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/no-rNO/user/translate.md +++ b/docs/no-rNO/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/no-rNO/user/units-and-locale.md b/docs/no-rNO/user/units-and-locale.md index e98af28876..fca92e9136 100644 --- a/docs/no-rNO/user/units-and-locale.md +++ b/docs/no-rNO/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/no-rNO/user/widget.md b/docs/no-rNO/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/no-rNO/user/widget.md +++ b/docs/no-rNO/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/pl-rPL/user/app-functions.md b/docs/pl-rPL/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/pl-rPL/user/app-functions.md +++ b/docs/pl-rPL/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/pl-rPL/user/connections.md b/docs/pl-rPL/user/connections.md index e5decc09cb..32f2d81d7d 100644 --- a/docs/pl-rPL/user/connections.md +++ b/docs/pl-rPL/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Sieć | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/pl-rPL/user/debug-logs.md b/docs/pl-rPL/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/pl-rPL/user/debug-logs.md +++ b/docs/pl-rPL/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/pl-rPL/user/desktop.md b/docs/pl-rPL/user/desktop.md index 3cdf48009b..1d44a57e06 100644 --- a/docs/pl-rPL/user/desktop.md +++ b/docs/pl-rPL/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/pl-rPL/user/discovery.md b/docs/pl-rPL/user/discovery.md index 5fa1e32e2e..a108dad32e 100644 --- a/docs/pl-rPL/user/discovery.md +++ b/docs/pl-rPL/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Opis | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Opis | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informacje o sąsiadze @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/pl-rPL/user/firmware.md b/docs/pl-rPL/user/firmware.md index ae1f390a66..ff8b12b4ed 100644 --- a/docs/pl-rPL/user/firmware.md +++ b/docs/pl-rPL/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/pl-rPL/user/help-and-docs.md b/docs/pl-rPL/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/pl-rPL/user/help-and-docs.md +++ b/docs/pl-rPL/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/pl-rPL/user/map-and-waypoints.md b/docs/pl-rPL/user/map-and-waypoints.md index 30d257d99e..fbb65e4e1b 100644 --- a/docs/pl-rPL/user/map-and-waypoints.md +++ b/docs/pl-rPL/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Warstwy map -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/pl-rPL/user/messages-and-channels.md b/docs/pl-rPL/user/messages-and-channels.md index 9e9c20d177..5db6ed2751 100644 --- a/docs/pl-rPL/user/messages-and-channels.md +++ b/docs/pl-rPL/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/pl-rPL/user/mqtt.md b/docs/pl-rPL/user/mqtt.md index 0b2dffe8c1..c588fb4470 100644 --- a/docs/pl-rPL/user/mqtt.md +++ b/docs/pl-rPL/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Wyłączony | | **TLS enabled** | Secure connection to broker | Wyłączony | | **Map reporting** | Report position to public map | Wyłączony | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Wyłączony | +| **Proxy to client enabled** | Relay MQTT through the connected app | Wyłączony | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/pl-rPL/user/node-metrics.md b/docs/pl-rPL/user/node-metrics.md index 889f129c56..ca848ab0d0 100644 --- a/docs/pl-rPL/user/node-metrics.md +++ b/docs/pl-rPL/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/pl-rPL/user/nodes.md b/docs/pl-rPL/user/nodes.md index f5492b0f62..0a3b90bf1d 100644 --- a/docs/pl-rPL/user/nodes.md +++ b/docs/pl-rPL/user/nodes.md @@ -1,8 +1,7 @@ --- title: Węzły -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Czujnik | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtr | Opis | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtr | Opis | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Opis | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Aktywność | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Odległość | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/pl-rPL/user/notifications.md b/docs/pl-rPL/user/notifications.md new file mode 100644 index 0000000000..21e4180ae9 --- /dev/null +++ b/docs/pl-rPL/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ---------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Wiadomości | Powiadomienia o bezpośredniej wiadomości | A message sent directly to you | The conversation | +| Wiadomości | Powiadomienia o wiadomościach rozgłoszeniowych | A message on one of your channels | The channel | +| Wiadomości | Powiadomienia punktów orientacyjnych | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Wiadomości | Powiadomienia alertowe | A critical alert from a node | The conversation | +| Mesh | Powiadomienia o nowych węzłach | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Powiadomienia o niskim poziomie baterii (ulubione węzły) | A favorite node's battery running low | The node's details | +| Urządzenie | Powiadomienia o usługach | The connection to your node while the app runs in the background | The app | +| Urządzenie | Powiadomienia o niskim poziomie baterii | Your node's battery running low | The node's details | +| Urządzenie | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Urządzenie | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/pl-rPL/user/onboarding.md b/docs/pl-rPL/user/onboarding.md index fd9d6ec8b8..7b01c5c3b7 100644 --- a/docs/pl-rPL/user/onboarding.md +++ b/docs/pl-rPL/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/pl-rPL/user/settings-module-admin.md b/docs/pl-rPL/user/settings-module-admin.md index 3f393d0d26..d35c0509b9 100644 --- a/docs/pl-rPL/user/settings-module-admin.md +++ b/docs/pl-rPL/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Konfiguracja modułu +## Ustawienia modułu Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Opis | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Włącz MQTT | Toggle MQTT bridge | -| Adres | MQTT broker address | -| Nazwa użytkownika | Authentication username | -| Hasło | Authentication password | -| Szyfrowanie włączone | Encrypt MQTT payloads | -| Włącz wyjście JSON | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| Włącz TLS | Use secure connection | -| Główny wątek | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Raportowanie map | Publish position to the public map — see the Map reporting group that follows | +| Setting | Opis | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Włącz MQTT | Toggle MQTT bridge | +| Adres | MQTT broker address | +| Nazwa użytkownika | Authentication username | +| Hasło | Authentication password | +| Szyfrowanie włączone | Encrypt MQTT payloads | +| Włącz wyjście JSON | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| Włącz TLS | Use secure connection | +| Główny wątek | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Raportowanie map | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Opis | -| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Zgadzam się. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Interwał raportowania map (sekundy) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Opis | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Zgadzam się. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +75,9 @@ Enables serial port communication for external device integrations (GPS modules, | Limit czasu | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,11 +93,11 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | Wyjście silnika wibracyjnego (GPIO) | Pin the vibration motor is wired to | | Użyj buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | | Użyj I2S jako buzzer | Send the alert through an I2S audio output instead | -| Czas trwania (w milisekundach) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| Nag Czas przerwy | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | Dzwonek | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Opis | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Opis | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Opis | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Włącz moduł metryk jakości powietrza | Report particulate and CO₂ sensor data | -| Czas aktualizacji metryk jakości powietrza | How often to report them | -| Włącz moduł metryk zasilania | Report the per-channel voltage and current readings | -| Czas aktualizacji metryk zasilania | How often to report them | -| Wyświetlaj metryki zasilania na ekranie | Also show power readings on the device's display | +| Setting | Opis | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Włącz moduł metryk jakości powietrza | Report particulate and CO₂ sensor data | +| Czas aktualizacji metryk jakości powietrza | How often to report them | +| Włącz moduł metryk zasilania | Report the per-channel voltage and current readings | +| Czas aktualizacji metryk zasilania | How often to report them | +| Wyświetlaj metryki zasilania na ekranie | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Opis | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Opis | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Dostępne piny | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Opis | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Włącz informacje o sąsiedzie | Activate neighbor broadcasting | -| Częstotliwość aktualizacji (w sekundach) | How often to broadcast neighbor list | -| Nadaj przez LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Opis | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Włącz informacje o sąsiedzie | Activate neighbor broadcasting | +| Częstotliwość aktualizacji | How often to broadcast neighbor list | +| Nadaj przez LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Natężenie | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Opis | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| Czujnik detekcji włączony | Activate detection sensor | -| Pin GPIO do monitorowania | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Użyj trybu INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Minimalny czas transmisji (sekundy) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Przyjazna nazwa | Custom name for this sensor | +| Setting | Opis | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Czujnik detekcji włączony | Activate detection sensor | +| Pin GPIO do monitorowania | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Użyj trybu INPUT_PULLUP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Przyjazna nazwa | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Opis | -| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Częstotliwość aktualizacji (w sekundach) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Opis | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Częstotliwość aktualizacji | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Uruchom ponownie | Restarts the radio | -| Wyłącz | Powers the radio down | -| Ustawienia fabryczne | Returns every setting to its factory default | -| Zresetuj NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Uruchom ponownie | Restarts the node | +| Wyłącz | Powers the node down | +| Ustawienia fabryczne | Returns every setting to its factory default | +| Zresetuj NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Kopia zapasowa i przywracanie -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Zaawansowane **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Wyczyść bazę węzłów -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O aplikacji @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/pl-rPL/user/settings-radio-user.md b/docs/pl-rPL/user/settings-radio-user.md index 0dd11884c1..b351c602be 100644 --- a/docs/pl-rPL/user/settings-radio-user.md +++ b/docs/pl-rPL/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ustawienia - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Opis | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Długa nazwa | Your display name (up to 39 characters) | -| Krótka nazwa | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Nie przyjmuje wiadomości | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Opis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Długa nazwa | Your display name (up to 39 characters) | +| Krótka nazwa | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Nie przyjmuje wiadomości | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Tryb retransmisji | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Interwał transmisji informacji o węźle | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Podwójne dotknięcie jako naciśnięcie przycisku | Treat a double tap as a button press | Wyłączony | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Wyłączony | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Włączony | | LED bicia serca | Blink the status LED periodically | Włączony | | Strefa czasowa | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Opis | Domyślny | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presety | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Moc nadawania | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Użyj predefiniowanych ustawień | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Szybkość kodowania | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Pasmo | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Slot częstotliwości | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmisja włączona | Turning this off makes the node receive-only | On | -| Nadpisz cykl pracy | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Wyłączony | -| Zignoruj MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok dla MQTT | Allow your packets to be forwarded to MQTT by gateways | Wyłączony | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Wyłączony | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Wyłączony | +| Setting | Opis | Domyślny | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presety | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Użyj predefiniowanych ustawień | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Szybkość kodowania | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Pasmo | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Slot częstotliwości | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmisja włączona | Turning this off makes the node receive-only | On | +| Nadpisz cykl pracy | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Wyłączony | +| Zignoruj MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok dla MQTT | Allow your packets to be forwarded to MQTT by gateways | Wyłączony | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Wyłączony | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Wyłączony | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Konfiguracja wyświetlacza -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Opis | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Ekran włączony na | How long the display stays lit before sleeping | -| Interwał karuzeli | How often the radio cycles between screens on its own | -| Tryb wyświetlania | Screen layout/density used by the firmware | -| Wyświetlana jednostka | Metric or Imperial on the radio's screen | -| Użyj formatu 12-godzinnego | Show the radio's clock as 12-hour rather than 24-hour | -| Pogrubiony nagłówek | Draw the screen's heading text in bold | -| Odwróć ekran | Rotate the display 180° for an inverted mounting | -| Typ ekranu OLED | Auto, SSD1306, SH1106, SH1107 | -| Wybudź przy dotknięciu lub ruchu | Light the screen when the radio is tapped or moved | -| Orientacja kompasu | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Zawsze wskazywać na północ | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Opis | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ekran włączony na | How long the display stays lit before sleeping | +| Interwał karuzeli | How often the node cycles between screens on its own | +| Tryb wyświetlania | Screen layout/density used by the firmware | +| Wyświetlana jednostka | Metric or Imperial on the node's screen | +| Użyj formatu 12-godzinnego | Show the node's clock as 12-hour rather than 24-hour | +| Pogrubiony nagłówek | Draw the screen's heading text in bold | +| Odwróć ekran | Rotate the display 180° for an inverted mounting | +| Typ ekranu OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wybudź przy dotknięciu lub ruchu | Light the screen when the node is tapped or moved | +| Orientacja kompasu | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Zawsze wskazywać na północ | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Konfiguracja pozycjonowania On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Interwał transmisji | How often the position is shared with the mesh | | Inteligentne Pozycjonowanie | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Opis | | ------------------------------------------------ | --------------------------------------------------------------- | -| Włącz tryb oszczędzania energii | Let the radio sleep aggressively between activity | +| Włącz tryb oszczędzania energii | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Konfiguracja sieci -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Opis | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Hasło | Hasło do sieci | | Ethernet włączony | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Konfiguracja Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Opis | | ------------------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Klucz publiczny | Your node's public key (read-only) | | Klucz administratora | Keys permitted to administer this node remotely — up to three | -| Klucz prywatny | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Klucz prywatny | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Klucze zapasowe | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/pl-rPL/user/signal-meter.md b/docs/pl-rPL/user/signal-meter.md index 13dce09251..993ea46845 100644 --- a/docs/pl-rPL/user/signal-meter.md +++ b/docs/pl-rPL/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/pl-rPL/user/tak.md b/docs/pl-rPL/user/tak.md index f587000f36..66add52607 100644 --- a/docs/pl-rPL/user/tak.md +++ b/docs/pl-rPL/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/pl-rPL/user/telemetry-and-sensors.md b/docs/pl-rPL/user/telemetry-and-sensors.md index daf17853e1..c4c620f632 100644 --- a/docs/pl-rPL/user/telemetry-and-sensors.md +++ b/docs/pl-rPL/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Czujnik | Metric | Notatki | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Czujnik | Metric | Notatki | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Czujnik | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Promieniowanie | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Odległość | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Promieniowanie | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Odległość | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Metryki zasilania diff --git a/docs/pl-rPL/user/translate.md b/docs/pl-rPL/user/translate.md index 6d2288e419..63cfd4ab6d 100644 --- a/docs/pl-rPL/user/translate.md +++ b/docs/pl-rPL/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/pl-rPL/user/units-and-locale.md b/docs/pl-rPL/user/units-and-locale.md index b2bc246eb3..5c7938f33b 100644 --- a/docs/pl-rPL/user/units-and-locale.md +++ b/docs/pl-rPL/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/pl-rPL/user/widget.md b/docs/pl-rPL/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/pl-rPL/user/widget.md +++ b/docs/pl-rPL/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/pt-rBR/user/app-functions.md b/docs/pt-rBR/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/pt-rBR/user/app-functions.md +++ b/docs/pt-rBR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/pt-rBR/user/connections.md b/docs/pt-rBR/user/connections.md index 1fe50688f0..b46b6fa625 100644 --- a/docs/pt-rBR/user/connections.md +++ b/docs/pt-rBR/user/connections.md @@ -1,8 +1,7 @@ --- title: Conexões -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Rede | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/pt-rBR/user/debug-logs.md b/docs/pt-rBR/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/pt-rBR/user/debug-logs.md +++ b/docs/pt-rBR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/pt-rBR/user/desktop.md b/docs/pt-rBR/user/desktop.md index c8a13e8d02..42002fddde 100644 --- a/docs/pt-rBR/user/desktop.md +++ b/docs/pt-rBR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/pt-rBR/user/discovery.md b/docs/pt-rBR/user/discovery.md index 5a792ebe8e..255311a09a 100644 --- a/docs/pt-rBR/user/discovery.md +++ b/docs/pt-rBR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descrição | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descrição | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informações do Vizinho @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/pt-rBR/user/firmware.md b/docs/pt-rBR/user/firmware.md index b8d0c47a14..9eceb9e104 100644 --- a/docs/pt-rBR/user/firmware.md +++ b/docs/pt-rBR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/pt-rBR/user/help-and-docs.md b/docs/pt-rBR/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/pt-rBR/user/help-and-docs.md +++ b/docs/pt-rBR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/pt-rBR/user/map-and-waypoints.md b/docs/pt-rBR/user/map-and-waypoints.md index eb83cd0514..baa82b2dd6 100644 --- a/docs/pt-rBR/user/map-and-waypoints.md +++ b/docs/pt-rBR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Camadas do Mapa -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/pt-rBR/user/messages-and-channels.md b/docs/pt-rBR/user/messages-and-channels.md index 3d4e137868..a7e8708922 100644 --- a/docs/pt-rBR/user/messages-and-channels.md +++ b/docs/pt-rBR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/pt-rBR/user/mqtt.md b/docs/pt-rBR/user/mqtt.md index 549856c18d..4778904fe3 100644 --- a/docs/pt-rBR/user/mqtt.md +++ b/docs/pt-rBR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/pt-rBR/user/node-metrics.md b/docs/pt-rBR/user/node-metrics.md index 62c7790ff3..d41d541648 100644 --- a/docs/pt-rBR/user/node-metrics.md +++ b/docs/pt-rBR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/pt-rBR/user/nodes.md b/docs/pt-rBR/user/nodes.md index 500600fbc3..97add6fd48 100644 --- a/docs/pt-rBR/user/nodes.md +++ b/docs/pt-rBR/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nós -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtro | Descrição | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrição | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descrição | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Visto pela última vez | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distância | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/pt-rBR/user/notifications.md b/docs/pt-rBR/user/notifications.md new file mode 100644 index 0000000000..d16bbbcca8 --- /dev/null +++ b/docs/pt-rBR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Mensagens | Notificações de mensagem direta | A message sent directly to you | The conversation | +| Mensagens | Notificações de mensagem transmitida | A message on one of your channels | The channel | +| Mensagens | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Mensagens | Notificações de Alerta | A critical alert from a node | The conversation | +| Mesh | Novas notificações de nó | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Notificações de bateria fraca (nós favoritos) | A favorite node's battery running low | The node's details | +| Dispositivo | Notificações de serviço | The connection to your node while the app runs in the background | The app | +| Dispositivo | Notificações de bateria fraca | Your node's battery running low | The node's details | +| Dispositivo | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Dispositivo | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/pt-rBR/user/onboarding.md b/docs/pt-rBR/user/onboarding.md index 1edf64331e..b16ed39795 100644 --- a/docs/pt-rBR/user/onboarding.md +++ b/docs/pt-rBR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/pt-rBR/user/settings-module-admin.md b/docs/pt-rBR/user/settings-module-admin.md index d258aedf12..a1b18366cb 100644 --- a/docs/pt-rBR/user/settings-module-admin.md +++ b/docs/pt-rBR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configuração de módulos Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Descrição | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Saída JSON ativada | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS ativado | Use secure connection | | Tópico principal | Base MQTT topic path | -| Proxy para cliente ativado | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy para cliente ativado | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Relatório de mapa | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descrição | -| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Eu concordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervalo de relatório de mapa (segundos) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descrição | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Eu concordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Tempo esgotado | How long to wait before considering an incoming message complete | | Substituir porta série do console | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descrição | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notificação Externa habilitada | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED de Saída (GPIO) | Pin the LED is wired to | -| LED de saída habilitado alto | Whether the LED pin is active high or low | -| Saída Campainha (GPIO) | Pin the buzzer is wired to | -| Vibra de saída (GPIO) | Pin the vibration motor is wired to | -| Usar uma campainha PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Usar I2S como campainha | Send the alert through an I2S audio output instead | -| Duração da Saída (milissegundos) | How long a single alert lasts | -| Tempo limite do Nag (segundos) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Toque | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descrição | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Notificação Externa habilitada | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED de Saída (GPIO) | Pin the LED is wired to | +| LED de saída habilitado alto | Whether the LED pin is active high or low | +| Saída Campainha (GPIO) | Pin the buzzer is wired to | +| Vibra de saída (GPIO) | Pin the vibration motor is wired to | +| Usar uma campainha PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Usar I2S como campainha | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Toque | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descrição | -| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| Teste de distância ativado | Activate range testing | -| Intervalo de mensagem do remetente (segundos) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Salvar .CSV no armazenamento (apenas ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descrição | +| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| Teste de distância ativado | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Salvar .CSV no armazenamento (apenas ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descrição | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Módulo de métricas do ambiente ativado | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Métricas de ambiente na tela habilitado | Also show these readings on the device's own display | -| Métricas de Ambiente usam Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Módulo de métricas de qualidade do ar habilitado | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Módulo de métricas de energia habilitado | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Métricas de ambiente na tela habilitado | Also show power readings on the device's display | +| Setting | Descrição | +| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Módulo de métricas do ambiente ativado | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Métricas de ambiente na tela habilitado | Also show these readings on the device's own display | +| Métricas de Ambiente usam Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Módulo de métricas de qualidade do ar habilitado | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Módulo de métricas de energia habilitado | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Métricas de ambiente na tela habilitado | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descrição | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Entrada Cima/Baixo/Selecionar ativada | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descrição | | ---------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Permitir acesso indefinido ao pino | Allow access to any GPIO pin (security risk) | | Pinos disponíveis | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descrição | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| Informações do Vizinho ativado | Activate neighbor broadcasting | -| Intervalo de atualização (segundos) | How often to broadcast neighbor list | -| Transmitir por LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descrição | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Informações do Vizinho ativado | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmitir por LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Atual | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Descrição | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Sensor de Detecção ativado | Activate detection sensor | -| Pino GPIO para monitorar | GPIO pin connected to sensor | -| Tipo de gatilho de deteção | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Usar o modo INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Transmissão mínima (segundos) | Minimum time between alert broadcasts | -| Transmissão de estado (segundos) | Periodic state broadcast interval | -| Enviar sino com mensagem de alerta | Include bell character in alerts | -| Nome amigável | Custom name for this sensor | +| Setting | Descrição | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Sensor de Detecção ativado | Activate detection sensor | +| Pino GPIO para monitorar | GPIO pin connected to sensor | +| Tipo de gatilho de deteção | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Usar o modo INPUT_PULLUP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Enviar sino com mensagem de alerta | Include bell character in alerts | +| Nome amigável | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descrição | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| Contador de Pessoas ativado | Activate people counting | -| Intervalo de atualização (segundos) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descrição | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Contador de Pessoas ativado | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reiniciar | Restarts the radio | -| Desligar | Powers the radio down | -| Redefinição de fábrica | Returns every setting to its factory default | -| Redefinir NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reiniciar | Restarts the node | +| Desligar | Powers the node down | +| Redefinição de fábrica | Returns every setting to its factory default | +| Redefinir NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup e Restaurar -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avançado **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Limpar Banco de Dados de Nó -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Sobre @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/pt-rBR/user/settings-radio-user.md b/docs/pt-rBR/user/settings-radio-user.md index 95ada23a3f..c88ef753d0 100644 --- a/docs/pt-rBR/user/settings-radio-user.md +++ b/docs/pt-rBR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - configurações - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descrição | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Impossível enviar mensagens | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descrição | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Impossível enviar mensagens | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descrição | Padrão | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Largura da banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Ignorar ciclo de trabalho | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignorar MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| Ventilador do PA desativado | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descrição | Padrão | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Largura da banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Ignorar ciclo de trabalho | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignorar MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| Ventilador do PA desativado | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configuração da Tela -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descrição | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Modo da tela | Screen layout/density used by the firmware | -| Unidades de exibição | Metric or Imperial on the radio's screen | -| Usar formato de relógio 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Inverter tela | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Orientação da bússola | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descrição | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Modo da tela | Screen layout/density used by the firmware | +| Unidades de exibição | Metric or Imperial on the node's screen | +| Usar formato de relógio 12h | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Inverter tela | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Orientação da bússola | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configuração da Posição On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrição | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descrição | | ---------------------------------------------------- | --------------------------------------------------------------- | -| Ativar modo de economia de energia | Let the radio sleep aggressively between activity | +| Ativar modo de economia de energia | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | Alterar proporção do multiplicador ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configuração de Rede -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descrição | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Senha | Network password | | Ethernet ativado | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configuração do Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descrição | | -------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrição | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chave Publica | Your node's public key (read-only) | | Chave do Administrador | Keys permitted to administer this node remotely — up to three | -| Chave Privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Chave Privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerar a chave privada | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Console serial | Serial console over the Stream API | -| API de logs de depuração ativada | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Modo Administrado | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API de logs de depuração ativada | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Modo Administrado | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/pt-rBR/user/signal-meter.md b/docs/pt-rBR/user/signal-meter.md index f9c17bdd15..7798beab21 100644 --- a/docs/pt-rBR/user/signal-meter.md +++ b/docs/pt-rBR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/pt-rBR/user/tak.md b/docs/pt-rBR/user/tak.md index df6048197e..fdc9d28878 100644 --- a/docs/pt-rBR/user/tak.md +++ b/docs/pt-rBR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/pt-rBR/user/telemetry-and-sensors.md b/docs/pt-rBR/user/telemetry-and-sensors.md index 85baf358e1..f72420c942 100644 --- a/docs/pt-rBR/user/telemetry-and-sensors.md +++ b/docs/pt-rBR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiação | µR/h | Card and chart | -| Peso | kg or lb | Card only — load cells, such as a beehive scale | -| Distância | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiação | µR/h | Card and chart | +| Peso | kg or lb | Card only — load cells, such as a beehive scale | +| Distância | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/pt-rBR/user/translate.md b/docs/pt-rBR/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/pt-rBR/user/translate.md +++ b/docs/pt-rBR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/pt-rBR/user/units-and-locale.md b/docs/pt-rBR/user/units-and-locale.md index b21379578b..a8c0bb8e95 100644 --- a/docs/pt-rBR/user/units-and-locale.md +++ b/docs/pt-rBR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/pt-rBR/user/widget.md b/docs/pt-rBR/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/pt-rBR/user/widget.md +++ b/docs/pt-rBR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/pt-rPT/user/app-functions.md b/docs/pt-rPT/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/pt-rPT/user/app-functions.md +++ b/docs/pt-rPT/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/pt-rPT/user/connections.md b/docs/pt-rPT/user/connections.md index 5a97bdec81..40595e1251 100644 --- a/docs/pt-rPT/user/connections.md +++ b/docs/pt-rPT/user/connections.md @@ -1,8 +1,7 @@ --- title: Ligações -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Rede | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/pt-rPT/user/debug-logs.md b/docs/pt-rPT/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/pt-rPT/user/debug-logs.md +++ b/docs/pt-rPT/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/pt-rPT/user/desktop.md b/docs/pt-rPT/user/desktop.md index 3de0c40438..b2908ae362 100644 --- a/docs/pt-rPT/user/desktop.md +++ b/docs/pt-rPT/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/pt-rPT/user/discovery.md b/docs/pt-rPT/user/discovery.md index 59cd9ea9b0..b402cbb08b 100644 --- a/docs/pt-rPT/user/discovery.md +++ b/docs/pt-rPT/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descrição | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descrição | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informações da vizinhança @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/pt-rPT/user/firmware.md b/docs/pt-rPT/user/firmware.md index b8d0c47a14..9eceb9e104 100644 --- a/docs/pt-rPT/user/firmware.md +++ b/docs/pt-rPT/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/pt-rPT/user/help-and-docs.md b/docs/pt-rPT/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/pt-rPT/user/help-and-docs.md +++ b/docs/pt-rPT/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/pt-rPT/user/map-and-waypoints.md b/docs/pt-rPT/user/map-and-waypoints.md index a9412a02f6..99fc70cbd1 100644 --- a/docs/pt-rPT/user/map-and-waypoints.md +++ b/docs/pt-rPT/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/pt-rPT/user/messages-and-channels.md b/docs/pt-rPT/user/messages-and-channels.md index aafe25657b..de2613a679 100644 --- a/docs/pt-rPT/user/messages-and-channels.md +++ b/docs/pt-rPT/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/pt-rPT/user/mqtt.md b/docs/pt-rPT/user/mqtt.md index 6e9c1a683d..d8c40f6194 100644 --- a/docs/pt-rPT/user/mqtt.md +++ b/docs/pt-rPT/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/pt-rPT/user/node-metrics.md b/docs/pt-rPT/user/node-metrics.md index d75899bd0a..749f632705 100644 --- a/docs/pt-rPT/user/node-metrics.md +++ b/docs/pt-rPT/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/pt-rPT/user/nodes.md b/docs/pt-rPT/user/nodes.md index 5f1a7abe32..ba2ad67213 100644 --- a/docs/pt-rPT/user/nodes.md +++ b/docs/pt-rPT/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK — ‘Kit’ de Consciencialização da Equipa | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Perdidos e Achados | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Perdidos e Achados | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtrar | Descrição | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtrar | Descrição | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descrição | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Último recebido | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distância | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/pt-rPT/user/notifications.md b/docs/pt-rPT/user/notifications.md new file mode 100644 index 0000000000..d5e96241b0 --- /dev/null +++ b/docs/pt-rPT/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------- | +| Mensagens | Direct message notifications | A message sent directly to you | The conversation | +| Mensagens | Broadcast message notifications | A message on one of your channels | The channel | +| Mensagens | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Mensagens | Notificações de alerta | A critical alert from a node | The conversation | +| Mesh | Notificações de novos nodes | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Notificações de bateria fraca (nodes favoritos) | A favorite node's battery running low | The node's details | +| Dispositivo | Notificações de serviço | The connection to your node while the app runs in the background | The app | +| Dispositivo | Notificação de bateria fraca | Your node's battery running low | The node's details | +| Dispositivo | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Dispositivo | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/pt-rPT/user/onboarding.md b/docs/pt-rPT/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/pt-rPT/user/onboarding.md +++ b/docs/pt-rPT/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/pt-rPT/user/settings-module-admin.md b/docs/pt-rPT/user/settings-module-admin.md index 3b9ba7d28e..7d302f2d1e 100644 --- a/docs/pt-rPT/user/settings-module-admin.md +++ b/docs/pt-rPT/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configurações dos módulos Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Descrição | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Saída JSON ativada | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | Ativar TLS | Use secure connection | | Tópico principal | Base MQTT topic path | -| Enviar através do cliente | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Enviar através do cliente | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Enviar para o mapa | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descrição | -| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Estou de acordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervalo de envio (segundos) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descrição | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Estou de acordo. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Substituir porta série do console | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descrição | -| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | -| Ativar notificações externas | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED de Saída (GPIO) | Pin the LED is wired to | -| LED de saída ativo alto | Whether the LED pin is active high or low | -| Buzzer de saída (GPIO) | Pin the buzzer is wired to | -| Vibra de saída (GPIO) | Pin the vibration motor is wired to | -| Usar um buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Usar I2S como buzzer | Send the alert through an I2S audio output instead | -| Duração da Saída (milissegundos) | How long a single alert lasts | -| Tempo limite a incomodar (segundos) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Toque | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descrição | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Ativar notificações externas | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED de Saída (GPIO) | Pin the LED is wired to | +| LED de saída ativo alto | Whether the LED pin is active high or low | +| Buzzer de saída (GPIO) | Pin the buzzer is wired to | +| Vibra de saída (GPIO) | Pin the vibration motor is wired to | +| Usar um buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Usar I2S como buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Toque | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descrição | -| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Ativar Teste de alcance | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Guardar .CSV no armazenamento (apenas ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descrição | +| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Ativar Teste de alcance | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Guardar .CSV no armazenamento (apenas ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descrição | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Módulo de métricas de ambiente ativado | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Mostrar métricas de ambiente no ecrã | Also show these readings on the device's own display | -| Métricas de Ambiente usam Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Módulo de métricas de qualidade do ar ativado | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Módulo de métricas de energia ativado | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Mostrar métricas de energia no ecrã | Also show power readings on the device's display | +| Setting | Descrição | +| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Módulo de métricas de ambiente ativado | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Mostrar métricas de ambiente no ecrã | Also show these readings on the device's own display | +| Métricas de Ambiente usam Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Módulo de métricas de qualidade do ar ativado | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Módulo de métricas de energia ativado | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Mostrar métricas de energia no ecrã | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descrição | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Entrada Cima/Baixo/Selecionar ativa | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descrição | | --------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Permitir acesso indefinido ao pin | Allow access to any GPIO pin (security risk) | | Pins disponíveis | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descrição | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| Enviar informações de vizinhos | Activate neighbor broadcasting | -| Intervalo de atualização (segundos) | How often to broadcast neighbor list | -| Enviar por LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descrição | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Enviar informações de vizinhos | Activate neighbor broadcasting | +| Intervalo de Atualização | How often to broadcast neighbor list | +| Enviar por LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Atual | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Descrição | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Sensor de deteção ativado | Activate detection sensor | -| Pin GPIO para monitorizar | GPIO pin connected to sensor | -| Tipo de gatilho de deteção | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Usar o modo INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Transmissão mínima (segundos) | Minimum time between alert broadcasts | -| Transmissão de estado (segundos) | Periodic state broadcast interval | -| Enviar sino com mensagem de alerta | Include bell character in alerts | -| Nome amigável | Custom name for this sensor | +| Setting | Descrição | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Sensor de deteção ativado | Activate detection sensor | +| Pin GPIO para monitorizar | GPIO pin connected to sensor | +| Tipo de gatilho de deteção | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Usar o modo INPUT_PULLUP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Enviar sino com mensagem de alerta | Include bell character in alerts | +| Nome amigável | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descrição | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| Ativar contador de pessoas | Activate people counting | -| Intervalo de atualização (segundos) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descrição | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Ativar contador de pessoas | Activate people counting | +| Intervalo de Atualização | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reiniciar | Restarts the radio | -| Desligar | Powers the radio down | -| Redefinição de fábrica | Returns every setting to its factory default | -| Redefinir NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reiniciar | Restarts the node | +| Desligar | Powers the node down | +| Redefinição de fábrica | Returns every setting to its factory default | +| Redefinir NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restauro -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Sobre @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/pt-rPT/user/settings-radio-user.md b/docs/pt-rPT/user/settings-radio-user.md index 334d8e94a1..03d2ec31fd 100644 --- a/docs/pt-rPT/user/settings-radio-user.md +++ b/docs/pt-rPT/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - definições - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descrição | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Impossível enviar mensagens | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descrição | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Impossível enviar mensagens | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descrição | Predefinição | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Largura de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Ignorar ciclo de trabalho | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignorar MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descrição | Predefinição | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Largura de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Ignorar ciclo de trabalho | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignorar MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Configuração do Ecrã -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descrição | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Modo de visualização | Screen layout/density used by the firmware | -| Unidade de visualização | Metric or Imperial on the radio's screen | -| Usar formato de relógio 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Inverter ecrã | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Orientação da bússola | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descrição | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Modo de visualização | Screen layout/density used by the firmware | +| Unidade de visualização | Metric or Imperial on the node's screen | +| Usar formato de relógio 12h | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Inverter ecrã | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Orientação da bússola | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Configuração da posição On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrição | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Intervalo de difusão | How often the position is shared with the mesh | | Posição Inteligente | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descrição | | ---------------------------------------------------- | --------------------------------------------------------------- | -| Ativar modo de poupança de energia | Let the radio sleep aggressively between activity | +| Ativar modo de poupança de energia | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | Alterar rácio do multiplicador ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Configuração de Rede -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descrição | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Palavra-passe | Network password | | Ethernet ativada | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configuração de Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descrição | | ---------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descrição | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chave pública | Your node's public key (read-only) | | Chave do Administrador | Keys permitted to administer this node remotely — up to three | -| Chave privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Chave privada | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Consola de série | Serial console over the Stream API | -| API de histórico de depuração ativada | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Modo Administrado | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API de histórico de depuração ativada | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Modo Administrado | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/pt-rPT/user/signal-meter.md b/docs/pt-rPT/user/signal-meter.md index 68f44f9119..f4bac8194d 100644 --- a/docs/pt-rPT/user/signal-meter.md +++ b/docs/pt-rPT/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/pt-rPT/user/tak.md b/docs/pt-rPT/user/tak.md index b9e64b3927..30d46cace7 100644 --- a/docs/pt-rPT/user/tak.md +++ b/docs/pt-rPT/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/pt-rPT/user/telemetry-and-sensors.md b/docs/pt-rPT/user/telemetry-and-sensors.md index 4c36b4827b..879944d8d2 100644 --- a/docs/pt-rPT/user/telemetry-and-sensors.md +++ b/docs/pt-rPT/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiação | µR/h | Card and chart | -| Peso | kg or lb | Card only — load cells, such as a beehive scale | -| Distância | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiação | µR/h | Card and chart | +| Peso | kg or lb | Card only — load cells, such as a beehive scale | +| Distância | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/pt-rPT/user/translate.md b/docs/pt-rPT/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/pt-rPT/user/translate.md +++ b/docs/pt-rPT/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/pt-rPT/user/units-and-locale.md b/docs/pt-rPT/user/units-and-locale.md index 9fb3e9e3fd..218d548661 100644 --- a/docs/pt-rPT/user/units-and-locale.md +++ b/docs/pt-rPT/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/pt-rPT/user/widget.md b/docs/pt-rPT/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/pt-rPT/user/widget.md +++ b/docs/pt-rPT/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ro-rRO/user/app-functions.md b/docs/ro-rRO/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/ro-rRO/user/app-functions.md +++ b/docs/ro-rRO/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/ro-rRO/user/connections.md b/docs/ro-rRO/user/connections.md index 211b6e6528..47fdbeea2c 100644 --- a/docs/ro-rRO/user/connections.md +++ b/docs/ro-rRO/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Rețea | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/ro-rRO/user/debug-logs.md b/docs/ro-rRO/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/ro-rRO/user/debug-logs.md +++ b/docs/ro-rRO/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/ro-rRO/user/desktop.md b/docs/ro-rRO/user/desktop.md index 85884d9ef0..5c8eb05ecc 100644 --- a/docs/ro-rRO/user/desktop.md +++ b/docs/ro-rRO/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/ro-rRO/user/discovery.md b/docs/ro-rRO/user/discovery.md index 0c0fce955e..c136bfda20 100644 --- a/docs/ro-rRO/user/discovery.md +++ b/docs/ro-rRO/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Descriere | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Descriere | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informații vecin @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/ro-rRO/user/firmware.md b/docs/ro-rRO/user/firmware.md index 3152551421..563cc049cc 100644 --- a/docs/ro-rRO/user/firmware.md +++ b/docs/ro-rRO/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ro-rRO/user/help-and-docs.md b/docs/ro-rRO/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/ro-rRO/user/help-and-docs.md +++ b/docs/ro-rRO/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/ro-rRO/user/map-and-waypoints.md b/docs/ro-rRO/user/map-and-waypoints.md index 59541f3eda..1e476bf1fd 100644 --- a/docs/ro-rRO/user/map-and-waypoints.md +++ b/docs/ro-rRO/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ro-rRO/user/messages-and-channels.md b/docs/ro-rRO/user/messages-and-channels.md index 7b78cec45a..d443b35c77 100644 --- a/docs/ro-rRO/user/messages-and-channels.md +++ b/docs/ro-rRO/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/ro-rRO/user/mqtt.md b/docs/ro-rRO/user/mqtt.md index d7f10b33e3..457a4ff9c8 100644 --- a/docs/ro-rRO/user/mqtt.md +++ b/docs/ro-rRO/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ro-rRO/user/node-metrics.md b/docs/ro-rRO/user/node-metrics.md index 377b6f05e6..5526e4e249 100644 --- a/docs/ro-rRO/user/node-metrics.md +++ b/docs/ro-rRO/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ro-rRO/user/nodes.md b/docs/ro-rRO/user/nodes.md index b78715b040..c9f2bdf516 100644 --- a/docs/ro-rRO/user/nodes.md +++ b/docs/ro-rRO/user/nodes.md @@ -1,8 +1,7 @@ --- title: Noduri -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Senzor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | Tracker TAK | TAK position reporting only | -| Pierdut și găsit | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Pierdut și găsit | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtru | Descriere | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtru | Descriere | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Descriere | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Ultima recepție | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distanță | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/ro-rRO/user/notifications.md b/docs/ro-rRO/user/notifications.md new file mode 100644 index 0000000000..3cb9c9b568 --- /dev/null +++ b/docs/ro-rRO/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ---------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Mesaje | Notificări mesaje directe | A message sent directly to you | The conversation | +| Mesaje | Notificări mesaje difuzate | A message on one of your channels | The channel | +| Mesaje | Notificări puncte de reper | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Mesaje | Notificări alerte | A critical alert from a node | The conversation | +| Mesh | Notificări noduri noi | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Notificări pentru baterii descărcate (noduri favorite) | A favorite node's battery running low | The node's details | +| Dispozitiv | Notificările serviciului | The connection to your node while the app runs in the background | The app | +| Dispozitiv | Notificări pentru baterii descărcate | Your node's battery running low | The node's details | +| Dispozitiv | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Dispozitiv | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ro-rRO/user/onboarding.md b/docs/ro-rRO/user/onboarding.md index 40be133c83..5795b9be4f 100644 --- a/docs/ro-rRO/user/onboarding.md +++ b/docs/ro-rRO/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/ro-rRO/user/settings-module-admin.md b/docs/ro-rRO/user/settings-module-admin.md index 74c77ebc4f..0924a0f2a8 100644 --- a/docs/ro-rRO/user/settings-module-admin.md +++ b/docs/ro-rRO/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Configurare modul Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Descriere | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Ieșire JSON activată | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS activat | Use secure connection | | Temă rădăcină | Base MQTT topic path | -| Proxy-ul pentru client activat | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy-ul pentru client activat | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Raportarea hărții | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Descriere | -| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sunt de acord | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Intervalul de raportare hartă (secunde) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Descriere | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sunt de acord | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Expirat | How long to wait before considering an incoming message complete | | Suprascrie portul serial al consolei | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Descriere | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Notificare externă activată | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| LED ieșire (GPIO) | Pin the LED is wired to | -| LED de ieșire activ ridicat | Whether the LED pin is active high or low | -| Buzzer ieșire (GPIO) | Pin the buzzer is wired to | -| Ieșire vibrație (GPIO) | Pin the vibration motor is wired to | -| Utilizează buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Utilizează I2S ca buzzer | Send the alert through an I2S audio output instead | -| Durată ieșire (milisecunde) | How long a single alert lasts | -| Durată notificare (secunde) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Sonerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Descriere | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Notificare externă activată | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| LED ieșire (GPIO) | Pin the LED is wired to | +| LED de ieșire activ ridicat | Whether the LED pin is active high or low | +| Buzzer ieșire (GPIO) | Pin the buzzer is wired to | +| Ieșire vibrație (GPIO) | Pin the vibration motor is wired to | +| Utilizează buzzer PWM | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Utilizează I2S ca buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Sonerie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Descriere | -| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Testul de gamă activat | Activate range testing | -| Interval mesaj expeditor (secunde) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Salvați .CSV doar în memorie (ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Descriere | +| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Testul de gamă activat | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Salvați .CSV doar în memorie (ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Descriere | -| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Trimite telemetrie dispozitiv | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Intervalul de actualizare a parametrilor dispozitivului | How often to report battery, uptime and channel utilization | -| Modul de măsurare mediu activat | Report the attached environment sensors | -| Interval actualizare valori mediu | How often to report them | -| Valorile de mediu pe ecran sunt activate | Also show these readings on the device's own display | -| Valorile de mediu utilizează Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Interval actualizare măsurători de calitate a aerului | How often to report them | -| Modul de măsurare putere activat | Report the per-channel voltage and current readings | -| Interval actualizare măsurători de putere | How often to report them | -| Valori pe ecran activate | Also show power readings on the device's display | +| Setting | Descriere | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Trimite telemetrie dispozitiv | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Intervalul de actualizare a parametrilor dispozitivului | How often to report battery, uptime and channel utilization | +| Modul de măsurare mediu activat | Report the attached environment sensors | +| Interval actualizare valori mediu | How often to report them | +| Valorile de mediu pe ecran sunt activate | Also show these readings on the device's own display | +| Valorile de mediu utilizează Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Interval actualizare măsurători de calitate a aerului | How often to report them | +| Modul de măsurare putere activat | Report the per-channel voltage and current readings | +| Interval actualizare măsurători de putere | How often to report them | +| Valori pe ecran activate | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Descriere | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Intrare sus/jos/selectare activată | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Descriere | | --------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Permite acces Pin nedefinit | Allow access to any GPIO pin (security risk) | | Pin-uri disponibile | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Descriere | -| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Info vecin activat | Activate neighbor broadcasting | -| Interval de actualizare (secunde) | How often to broadcast neighbor list | -| Transmite peste LoRA | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Descriere | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Info vecin activat | Activate neighbor broadcasting | +| Interval de actualizare GPS | How often to broadcast neighbor list | +| Transmite peste LoRA | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +215,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Actual | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -226,39 +225,39 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | Pin GPIO de monitorizat | GPIO pin connected to sensor | | Tip declanșator detectare | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | Folosește modul INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Difuzare minimă (secunde) | Minimum time between alert broadcasts | -| Difuzare stare (secunde) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Trimite clopoțelul cu mesaj de alertă | Include bell character in alerts | | Nume comun | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Descriere | -| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter activat | Activate people counting | -| Interval de actualizare (secunde) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Descriere | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter activat | Activate people counting | +| Interval de actualizare GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Restartează | Restarts the radio | -| Oprire | Powers the radio down | -| Resetare la setările din fabrică | Returns every setting to its factory default | -| Resetare NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Restartează | Restarts the node | +| Oprire | Powers the node down | +| Resetare la setările din fabrică | Returns every setting to its factory default | +| Resetare NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Copie de rezervă și restaurare -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Avansate **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Curăță baza de date a nodurilor -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Despre @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/ro-rRO/user/settings-radio-user.md b/docs/ro-rRO/user/settings-radio-user.md index 9df694d513..c552cc139f 100644 --- a/docs/ro-rRO/user/settings-radio-user.md +++ b/docs/ro-rRO/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - setari - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Descriere | -| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nume lung | Your display name (up to 39 characters) | -| Nume scurt | 4-character abbreviated name | -| Mesaj de stare: | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Netransmisibil | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Radioamator autorizat | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Descriere | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nume lung | Your display name (up to 39 characters) | +| Nume scurt | 4-character abbreviated name | +| Mesaj de stare: | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Netransmisibil | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Mod de redifuzare | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Intervalul de difuzare a informațiilor nodului | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Apăsare dublă ca buton | Treat a double tap as a button press | Disabled | -| Apăsare triplă pentru ping Ad Hoc | Send an ad-hoc position ping on a triple click | Disabled | +| Apăsare triplă pentru ping Ad Hoc | Send an ad-hoc position ping on a triple click | Activat | | Puls LED | Blink the status LED periodically | Activat | | Fus orar | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Descriere | Prestabilit | -| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regiune | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presetări | Speed/range tradeoff | LongFast | -| Numărul de Hops | Maximum retransmit hops | 3 | -| Putere transmisie | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Suprascriere frecvență | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Utilizare presetare | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Factor de răspândire | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Rata de codificare | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Lățime bandă | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Slot pentru frecvenţă | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmisie activată | Turning this off makes the node receive-only | On | -| Suprascrie ciclul de obligații | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignoră MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Acceptă MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| Amplificare RX amplificată | Extra receive gain on SX126x radios; costs a little current | Off | -| Ventilator PA dezactivat | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Descriere | Prestabilit | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regiune | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presetări | Speed/range tradeoff | LongFast | +| Numărul de Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Suprascriere frecvență | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Utilizare presetare | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Factor de răspândire | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Rata de codificare | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Lățime bandă | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Slot pentru frecvenţă | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmisie activată | Turning this off makes the node receive-only | On | +| Suprascrie ciclul de obligații | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignoră MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Acceptă MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| Amplificare RX amplificată | Extra receive gain on SX126x radios; costs a little current | Off | +| Ventilator PA dezactivat | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Descriere | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Ecran pornit pentru | How long the display stays lit before sleeping | -| Intervalul caruselului | How often the radio cycles between screens on its own | -| Mod ecran | Screen layout/density used by the firmware | -| Unități de măsură afișate | Metric or Imperial on the radio's screen | -| Utilizaţi formatul ceasului 12h | Show the radio's clock as 12-hour rather than 24-hour | -| Direcție îngroșat | Draw the screen's heading text in bold | -| Rotire ecran | Rotate the display 180° for an inverted mounting | -| Tip OLED | Auto, SSD1306, SH1106, SH1107 | -| Trezire la apăsare sau mișcare | Light the screen when the radio is tapped or moved | -| Orientarea busolei | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Indică mereu spre nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Descriere | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ecran pornit pentru | How long the display stays lit before sleeping | +| Intervalul caruselului | How often the node cycles between screens on its own | +| Mod ecran | Screen layout/density used by the firmware | +| Unități de măsură afișate | Metric or Imperial on the node's screen | +| Utilizaţi formatul ceasului 12h | Show the node's clock as 12-hour rather than 24-hour | +| Direcție îngroșat | Draw the screen's heading text in bold | +| Rotire ecran | Rotate the display 180° for an inverted mounting | +| Tip OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Trezire la apăsare sau mișcare | Light the screen when the node is tapped or moved | +| Orientarea busolei | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Indică mereu spre nord | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descriere | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Mod GPS (hardware fizic) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Interval de actualizare GPS | How often the radio asks its GPS for a fix | +| Interval de actualizare GPS | How often the node asks its GPS for a fix | | Interval de difuzare | How often the position is shared with the mesh | | Poziție inteligentă | Broadcast based on movement rather than purely on the clock | | Interval inteligent | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Descriere | | ----------------------------------------------- | --------------------------------------------------------------- | -| Activează modul de economisire a energiei | Let the radio sleep aggressively between activity | +| Activează modul de economisire a energiei | Let the node sleep aggressively between activity | | Închidere la pierderea de energie | Power the device down after external power disappears | | Durată maximă de somn | How long the deepest sleep state lasts | -| Durata minimă a trezirii | The shortest time the radio stays awake once woken | +| Durata minimă a trezirii | The shortest time the node stays awake once woken | | Așteptați pentru durata Bluetooth | How long to wait for a phone to connect before sleeping | | Suprascriere multiplicator ADC | Turn on a manual correction for battery-voltage readings | | Raportul suprascrierii multiplicatorului ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Descriere | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | Numele rețelei | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Parolă | Network password | | Ethernet activat | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Configurare Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Descriere | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Descriere | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chei publice | Your node's public key (read-only) | | Cheie Administrator | Keys permitted to administer this node remotely — up to three | -| Cheia privată | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Cheia privată | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerează Cheia privată | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Consolă serială | Serial console over the Stream API | -| Debug log API activat | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Mod Gestionat | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API activat | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Mod Gestionat | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ro-rRO/user/signal-meter.md b/docs/ro-rRO/user/signal-meter.md index 13ffcb8be4..7ee9d00976 100644 --- a/docs/ro-rRO/user/signal-meter.md +++ b/docs/ro-rRO/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/ro-rRO/user/tak.md b/docs/ro-rRO/user/tak.md index b04d2788d9..acad2e975e 100644 --- a/docs/ro-rRO/user/tak.md +++ b/docs/ro-rRO/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ro-rRO/user/telemetry-and-sensors.md b/docs/ro-rRO/user/telemetry-and-sensors.md index 0284c20e3e..12c1169315 100644 --- a/docs/ro-rRO/user/telemetry-and-sensors.md +++ b/docs/ro-rRO/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Senzor | Metric | Notițe | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Senzor | Metric | Notițe | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Senzor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiație | µR/h | Card and chart | -| Greutate | kg or lb | Card only — load cells, such as a beehive scale | -| Distanță | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiație | µR/h | Card and chart | +| Greutate | kg or lb | Card only — load cells, such as a beehive scale | +| Distanță | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Valori putere diff --git a/docs/ro-rRO/user/translate.md b/docs/ro-rRO/user/translate.md index 0e6196e5c3..0d4b8dfe74 100644 --- a/docs/ro-rRO/user/translate.md +++ b/docs/ro-rRO/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/ro-rRO/user/units-and-locale.md b/docs/ro-rRO/user/units-and-locale.md index fa072f3363..876ea526ab 100644 --- a/docs/ro-rRO/user/units-and-locale.md +++ b/docs/ro-rRO/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/ro-rRO/user/widget.md b/docs/ro-rRO/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/ro-rRO/user/widget.md +++ b/docs/ro-rRO/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/ru-rRU/index.md b/docs/ru-rRU/index.md index 8cbfd09d66..3ec360eddd 100644 --- a/docs/ru-rRU/index.md +++ b/docs/ru-rRU/index.md @@ -6,7 +6,7 @@ nav_order: 0 # Документация приложения Meshtastic Android -User and developer documentation for the Meshtastic Android and Desktop app, built with Kotlin Multiplatform. +Документация для пользователей и разработчиков приложения Meshtastic для Android и настольных компьютеров, созданного на Kotlin Multiplatform. Используйте боковую панель навигации, чтобы просматривать **Руководство пользователя** по функциям приложения и **Руководство разработчика** по внесению вклада в проект. diff --git a/docs/ru-rRU/user/app-functions.md b/docs/ru-rRU/user/app-functions.md index 5b0a7b488b..599d6c7aee 100644 --- a/docs/ru-rRU/user/app-functions.md +++ b/docs/ru-rRU/user/app-functions.md @@ -1,6 +1,5 @@ --- title: Функции приложения -parent: Руководство пользователя nav_order: 19 last_updated: 2026-08-30 description: Предоставьте возможности mesh системе Android и помощникам с ИИ на устройстве (например, Gemini), чтобы они могли выполнять mesh-воркфлоу без открытия приложения. @@ -13,50 +12,50 @@ aliases: # Функции приложения -Функции приложения предоставляют возможности Meshtastic системе Android и встроенным AI-помощникам (таким как Gemini) через API функций приложения Android. С их включением помощник может находить и запускать сетевые рабочие процессы за тебя — например, отправлять сообщение или проверять статус сети — не утруждая тебя открытием приложения. App Functions are available on **Google-flavor Android builds only**. +Функции приложения предоставляют возможности Meshtastic системе Android и встроенным AI-помощникам (таким как Gemini) через API функций приложения Android. С их включением помощник может находить и запускать сетевые рабочие процессы за тебя — например, отправлять сообщение или проверять статус сети — не утруждая тебя открытием приложения. Примечание:\*\* Функции приложения доступны только на **Android-версиях от Google**. -> ℹ️ **Note:** This is separate from the in-app **Chirpy** assistant. Функции приложения позволяют _системному_ AI-помощнику действовать с вашей сетью; Chirpy — это голосовой помощник прямо внутри приложения Meshtastic. +> ℹ️ **Примечание:** Это отличное от встроенного помощника **Chirpy**. Функции приложения позволяют _системному_ AI-помощнику действовать с вашей сетью; Chirpy — это голосовой помощник прямо внутри приложения Meshtastic. ## Включение функций приложения -Control App Functions from **Settings → System AI**. Экран содержит: +Управление функциями приложения из **Настройки → Система ИИ**. Экран содержит: - **Главный переключатель** с надписью **"Разрешить доступ ИИ"** и подзаголовком _"Позволь системным ИИ-ассистентам (например, Gemini) обнаруживать и использовать функции сети"_. Когда выключено, системе не доступны никакие функции. - **Отдельный переключатель для каждой функции**, чтобы включать только те возможности, которые хочешь. -> ⚠️ **Important:** App Functions ship switched on. On a Google-flavor build the master toggle and every individual function, **Send message** included, start enabled — so an assistant can read your mesh data and send messages to your mesh until you turn **Allow AI access** off. +> ⚠️ **Важно:** Функции приложения включены. В сборке Google главный переключатель и все отдельные функции, включая **Отправить сообщение**, включены по умолчанию — так что помощник может считывать данные из сети и отправлять сообщения, пока ты не отключишь **Доступ для ИИ**. Функции разделены на секцию **Запись** (функции, которые что-то меняют или отправляют данные в сеть) и **Чтение** (только возвращают информацию). ![Экран функций приложения с переключателями для всех функций и отдельных функций](../../assets/screenshots/app-functions_settings.png) -The screenshot has **Send message** and **Get recent messages** switched off to illustrate per-function control; a fresh install shows every switch on. +На скриншоте кнопки **Отправить сообщение** и **Получить последние сообщения** выключены, чтобы проиллюстрировать управление отдельными функциями. При первой установке все кнопки включены. ### Функции записи -| Функция | Что она делает | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Send message** | Sends a text message to a contact (direct message) or to a channel. The mesh carries at most 233 bytes of text, so keep assistant-composed messages short. | +| Функция | Что она делает | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Отправка сообщения** | Отправляет текстовое сообщение контакту (личное сообщение) или в канал. В сообщении может быть не более 233 байт текста, поэтому старайтся, чтобы сообщения, составленные помощником, были короткими. | ### Функции записи -| Функция | Что она возвращает | -| ----------------------- | ----------------------------------------------------------------------------------- | -| **Get mesh status** | Whether you're connected to a radio, and how many nodes are online. | -| **Get node list** | Список узлов в твоей сети. | -| **Get channel info** | Информация о твоих каналах. | -| **Get device status** | Состояние подключенного радиоустройства. | -| **Get node details** | Подробная информация о конкретной ноде. | -| **Get mesh metrics** | Телеметрия и метрики твоей сети. | -| **Get recent messages** | Последние сообщения из твоих разговоров. | -| **Get unread summary** | Сводка непрочитанных сообщений. | +| Функция | Что она возвращает | +| --------------------------------- | ------------------------------------------------------------------------ | +| **Получить статус сети** | Неважно, подключены ли ты к радио, и сколько нод в сети. | +| **Получить список нод** | Список узлов в твоей сети. | +| **Получить информацию о канале** | Информация о твоих каналах. | +| **Получить состояние устройства** | Состояние подключенного радиоустройства. | +| **Получить детали ноды** | Подробная информация о конкретной ноде. | +| **Получить метрики сети** | Телеметрия и метрики твоей сети. | +| **Получить последние сообщения** | Последние сообщения из твоих разговоров. | +| **Получить обзор непрочитанного** | Сводка непрочитанных сообщений. | ## Приватность -> 🔒 **Privacy:** The **Send message** function lets an assistant send messages to your mesh on your behalf, and the read functions expose node, message, and metric data to it. Because all of them start enabled, the choice you make here is what to turn off rather than what to turn on. Each function has its own toggle, and **Allow AI access** turns all of them off at once. +> 🔒 **Конфиденциальность:** Функция **Отправить сообщение** позволяет ассистенту отправлять сообщения в сеть от твоего имени, и функции чтения выявляют в ней ноды, сообщения и метрические данные. Поскольку все они изначально включены, тебе нужно решить, что отключить, а не что включить. Каждая функция имеет свой собственный переключатель, и **Разрешить доступ к ИИ** отключает все их одновременно. ## Связанные темы - [Сообщения и каналы](messages-and-channels) — отправка сообщений прямо в приложении - [Ноды](nodes) — список нод, из которого берут данные функции чтения -- [Node Metrics](node-metrics) — the telemetry behind Get mesh metrics +- [Node Metrics](node-metrics) — телеметрия, лежащая в основе функции Получить метрики сети diff --git a/docs/ru-rRU/user/connections.md b/docs/ru-rRU/user/connections.md index 2f3e567da3..4fc0ba7401 100644 --- a/docs/ru-rRU/user/connections.md +++ b/docs/ru-rRU/user/connections.md @@ -1,8 +1,7 @@ --- title: Соединения -parent: Руководство пользователя nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Подключи свой телефон или компьютер к устройству Meshtastic через Bluetooth, USB или TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ You can change the pairing method, or turn Bluetooth on for a radio that ships w The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | Что это значит | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | Что это значит | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Состояние подключения](../../assets/screenshots/connections_connecting.png) -Если устройства не найдены, приложение показывает пустое состояние с инструкциями: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![Устройства не найдены](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Сеть | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Устранение неполадок Bluetooth diff --git a/docs/ru-rRU/user/debug-logs.md b/docs/ru-rRU/user/debug-logs.md index b9da53b9c3..56655aee60 100644 --- a/docs/ru-rRU/user/debug-logs.md +++ b/docs/ru-rRU/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Отладочные журналы -parent: Инструкция пользователя nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Просматривайте и экспортируйте отладочные журналы приложения из самого приложения, а также прикрепляйте их к задаче на GitHub для помощи в диагностике ошибок — без необходимости в adb. aliases: - debug-logs @@ -48,7 +47,7 @@ The **App logs** tab shows the most recent log lines from **this app only** — ## Desktop -У компьютерного приложения нет системного logcat, поэтому вкладка **Журналы приложения** вместо этого показывает собственный захваченный вывод журнала приложения. Поиск, фильтрация и экспорт работают точно так же. +У компьютерного приложения нет системного logcat, поэтому вкладка **Журналы приложения** вместо этого показывает собственный захваченный вывод журнала приложения. Поиск, фильтрация и экспорт работают точно так же. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Связанные темы diff --git a/docs/ru-rRU/user/desktop.md b/docs/ru-rRU/user/desktop.md index 631283aa2b..2a09541f1c 100644 --- a/docs/ru-rRU/user/desktop.md +++ b/docs/ru-rRU/user/desktop.md @@ -1,6 +1,5 @@ --- title: Настольное приложение -parent: Руководство пользователя nav_order: 14 last_updated: 2026-09-11 description: Установка и использование приложения Meshtastic Desktop на Linux, macOS и Windows — подключения, функционал и сочетания клавиш. diff --git a/docs/ru-rRU/user/discovery.md b/docs/ru-rRU/user/discovery.md index 46b52afac5..4d25e6a151 100644 --- a/docs/ru-rRU/user/discovery.md +++ b/docs/ru-rRU/user/discovery.md @@ -1,8 +1,7 @@ --- title: Локальное обнаружение сети -parent: Руководство пользователя nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Исследуйте свою mesh-сеть — сканер локального обнаружения mesh-сети, трассировка путей, карты соседей и инструменты обнаружения нодов. aliases: - discovery @@ -20,18 +19,18 @@ aliases: Приложение предлагает два дополнительных подхода: -- **Локальное обнаружение mesh-сети (сканер)** — автоматический режим, который по очереди перебирает на твоём подключённом радио разные пресеты LoRa, слушает каждый и определяет, какой пресет лучше всего работает в твоём местоположении. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Ручное исследование** — трассировка, информация о соседях и список нод, которые ты можешь использовать в любое время для изучения конкретных путей и топологии. ## Обнаружение локальной сети (Сканер) -Локальное обнаружение mesh-сети — это специализированный режим сканирования, который помогает найти лучший пресет LoRa-модема для твоего местоположения и увидеть, какие ноды активны на каждом пресете. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Локальное обнаружение mesh-сети — это специализированный режим сканирования, который помогает найти лучший пресет LoRa-модема для твоего местоположения и увидеть, какие ноды активны на каждом пресете. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Настройка сканирования +### Setting up a scan Перед началом, настройте эти параметры: @@ -39,34 +38,34 @@ Connect your radio, then open **Settings → Advanced → Local Mesh Discovery** | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Выбор пресета LoRa** | Выберите один или несколько пресетов для сканирования. Обнаружение по очереди задерживается на каждом выбранном пресете. | | **Время задержки** | Время прослушки каждого пресета. Выбери один из вариантов: 1, 5, 15, 30, 45, 60, 90, 120 или 180 минут. Более долгое время задержки собирает больше пакетов и даёт более чёткую картину, но занимает больше времени. | -| **Не выключать экран** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| **Не выключать экран** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Распространённые причины, почему она отключена: -- The radio is **not connected**. +- The node is **not connected**. - **Не выбраны пресеты** для сканирования. - Выбранный пресет использует частоту **2,4 ГГц**, которую ваше оборудование не поддерживает. -### Текущий прогресс +### Live progress Во время сканирования "Обнаружение" показывает текущий этап: -| Этап | Что происходит | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Сохранение твоей текущей конфигурации и подготовка к сканированию. | -| **Переключение на \** | Переключение радио на следующий пресет для тестирования. | -| **Reconnecting on \** | Восстановление соединения после смены пресета. | -| **Dwelling on \** | Прослушивание текущего пресета для сбора пакетов с обратным отсчётом до следующего шага. | -| **Analyzing results** | Обработка собранных пакетов и ранжирование пресетов. | -| **Restoring home preset** | Возврат твоей исходной конфигурации LoRa. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Этап | Что происходит | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Сохранение твоей текущей конфигурации и подготовка к сканированию. | +| **Переключение на \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Восстановление соединения после смены пресета. | +| **Dwelling on \** | Прослушивание текущего пресета для сбора пакетов с обратным отсчётом до следующего шага. | +| **Analyzing results** | Обработка собранных пакетов и ранжирование пресетов. | +| **Restoring home preset** | Возврат твоей исходной конфигурации LoRa. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Обратный отсчёт времени прослушивания, показывающий оставшееся время на текущем пресете](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Чтение результатов +### Reading the results По завершении сканирования "Обнаружение" показывает карточку результатов для каждого протестированного пресета, а также общую сводку. @@ -94,28 +93,28 @@ If a scan is interrupted — the app is closed, or the radio goes away — the a Маяк mesh-сети позволяет нодам приглашать другие устройства присоединиться к своей сети. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Слушать маяки** — принимать приглашения, передаваемые другими нодами. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Полученные приглашения отображаются в виде карточек **"Приглашения mesh-сети"** на экране **"Обнаружение"**. На каждой карточке показано сообщение отправителя, а также предлагаемые канал, регион, пресет и качество сигнала, и доступны следующие действия: -- **Присоединиться** — переключиться на предлагаемый канал и пресет (перенастраивает радио и перезагружает его). Если предложение совпадает с вашим текущим частотным слотом, действие **"Добавить канал"** добавляет его без перезагрузки. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). Если предложение совпадает с вашим текущим частотным слотом, действие **"Добавить канал"** добавляет его без перезагрузки. - **Обнаружить** — запустить сканирование «Обнаружения» с предложенным пресетом, чтобы ты мог изучить эту сеть перед присоединением (отображается, только если маяк передаёт пресет). - **Отклонить** — проигнорировать приглашение. Каналы, объявленные маяками, также отображаются в настройках сканирования как **Каналы маяков** — выберите один, чтобы включить его в число целей сканирования. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Ручное исследование +## Manual exploration The following tools are available at any time from the node list and node detail screens. Используйте их для исследования конкретных путей и построения картины топологии — вместе с полным сканированием или вместо него. @@ -128,7 +127,7 @@ The following tools are available at any time from the node list and node detail 1. Перейди в **Ноды** и коснись ноды, которую ты хочешь отследить. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Чтение результатов +#### Reading the results Результат трассировки выглядит так: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| На что обращать внимание | Что это значит | -| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| Все хопы показывают хороший SNR (≥ −7 дБ, зелёный цвет) | Здоровый путь — сообщения идут без сбоев | -| One hop shows a poor SNR (below −15 dB, orange) | Слабое звено — этот сегмент ретрансляции хрупкий | -| Много хопов (4+) | Длинный путь — подумай о перемещении ноды для его сокращения | -| Другой путь при повторе | Сеть адаптируется — существуют несколько маршрутов (это хорошо!) | +| На что обращать внимание | Что это значит | +| ----------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Здоровый путь — сообщения идут без сбоев | +| One hop shows a poor SNR (orange or red) | Слабое звено — этот сегмент ретрансляции хрупкий | +| Много хопов (4+) | Длинный путь — подумай о перемещении ноды для его сокращения | +| Другой путь при повторе | Сеть адаптируется — существуют несколько маршрутов (это хорошо!) | > 💡 **Совет:** Запусти трассировку несколько раз в течение нескольких минут. Если путь изменяется, у твоей сети есть лишние маршруты — признак хорошо связанной сети. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Проверь, что обе ноды имеют хотя бы один канал с одинаковым ключом шифрования. - **Время ожидания трассировки истекло** — путь может быть слишком длинным (превышен лимит хопов) или нода ретрансляции перегружена. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Асимметричные маршруты** — трассировка от A→B может проходить по другому пути, чем B→A. Это нормально — распространение радиоволн не всегда симметрично. +- **Асимметричные маршруты** — трассировка от A→B может проходить по другому пути, чем B→A. This is normal — radio propagation isn't always symmetric. ### Информация об окружении @@ -168,42 +167,42 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Включение модуля -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Другие ноды с включённым модулем информации о соседях делают то же самое. -#### Просмотр данных соседа +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Каждая запись о соседе показывает ноду, которая была услышана напрямую, и качество её сигнала. - Объединяйте данные о соседях от нескольких нод, чтобы понять полную топологию mesh-сети. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Список узлов как инструмент для обзора +### Node list as a discovery tool Сам по себе список нод — мощный инструмент обнаружения, если ты эффективно будешь использовать его возможности фильтрации и сортировки. -#### Поиск новых узлов +#### Finding new nodes - Сортируйте по **"Последнему приёму"**, чтобы увидеть вверху списка ноды, активные в последнее время. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Оценка подключения +#### Assessing connectivity - Сортируйте по «Хопам», чтобы видеть, какие ноды доступны напрямую (0 хопов), а какие — через ретрансляцию. - Сортируйте по **"Расстоянию"**, чтобы найти близлежащие ноды и убедиться, что они доступны. -- Используйте **"Исключить MQTT"**, чтобы сосредоточиться на нодах, доступных по радио (а не через интернет-мост). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Аудит инфраструктуры +#### Infrastructure audit - Отключите **"Исключить инфраструктуру"**, чтобы увидеть ноды Router, Router Late и Client Base. - Проверьте качество их сигнала и время последнего приёма, чтобы убедиться, что твои ноды инфраструктуры работают исправно. См. раздел [Ноды](Nodes) для получения полной информации о параметрах фильтрации и сортировки. -## Советы по исследованию сети +## Tips for Mesh exploration - **Начните с трассировки** — она даёт тебе немедленную, пригодную для использования информацию о конкретном пути. - **Включите информацию о соседях на ключевых нодах** — особенно на роутерах и ретрансляторах, чтобы составить картину магистральной сети. diff --git a/docs/ru-rRU/user/firmware.md b/docs/ru-rRU/user/firmware.md index 0aef767eb0..bc2d4b214a 100644 --- a/docs/ru-rRU/user/firmware.md +++ b/docs/ru-rRU/user/firmware.md @@ -1,6 +1,5 @@ --- title: Обновления прошивки -parent: Руководство пользователя nav_order: 13 last_updated: 2026-09-06 description: Обновляйте прошивку своего радио по Bluetooth или USB — процесс OTA, каналы версий, предполётные проверки и восстановление. @@ -17,7 +16,7 @@ aliases: ## Проверка обновлений -1. Откройте конфигурацию подключённого радио и в разделе **"Дополнительно"** нажмите **"Обновление прошивки"**. The entry appears only for OTA-capable radios. +1. Откройте конфигурацию подключённого радио и в разделе **"Дополнительно"** нажмите **"Обновление прошивки"**. Этот пункт появляется только для устройств, поддерживающих OTA. 2. Приложение проверяет доступные версии прошивки. 3. Доступные обновления показывают номер версии и сводку изменений. @@ -27,12 +26,12 @@ aliases: Наиболее распространённый способ обновления для пользователей Android: -> ⚠️ **Warning:** Interrupting a firmware update can leave the radio unable to boot. Keep the phone nearby and both devices powered until the update completes. +> ⚠️ **Предупреждение:** Прерывание обновления прошивки может привести к невозможности загрузки устройства. Держите телефон рядом и оба устройства включенными до завершения обновления. 1. Убедитесь, что твоё радио подключено по Bluetooth. 2. Перейдите на экран "Обновление прошивки". 3. Выберите нужную версию прошивки. -4. Tap **Update**. An **Update Warning** dialog lists the pre-flight checks — read it, then tap **I know what I'm doing.** to start. This dialog appears for every update method, including Wi-Fi OTA, USB, and a local firmware file. +4. Нажмите **Обновить**. An **Update Warning** dialog lists the pre-flight checks — read it, then tap **I know what I'm doing.** to start. This dialog appears for every update method, including Wi-Fi OTA, USB, and a local firmware file. 5. Дождитесь завершения обновления — **не отключайте устройство** во время обновления. ![Проверка обновлений прошивки](../../assets/screenshots/firmware_checking.png) @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as Приложение считывает `INFO_UF2.TXT` с выбранного тобою диска, чтобы убедиться, что это действительно диск обновления устройства, и определить плату до записи чего-либо. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/ru-rRU/user/help-and-docs.md b/docs/ru-rRU/user/help-and-docs.md index 866b8d8b35..9881d4f52e 100644 --- a/docs/ru-rRU/user/help-and-docs.md +++ b/docs/ru-rRU/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Справка и встроенная документация -parent: Руководство пользователя nav_order: 21 last_updated: 2026-09-11 description: Просматривайте эту документацию внутри приложения, выполняйте по ней поиск и задавайте вопросы о Meshtastic ассистенту Chirpy — встроенному ИИ-помощнику на устройстве. diff --git a/docs/ru-rRU/user/map-and-waypoints.md b/docs/ru-rRU/user/map-and-waypoints.md index 9a7efda43b..87c79f7fbd 100644 --- a/docs/ru-rRU/user/map-and-waypoints.md +++ b/docs/ru-rRU/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Карта и путевые точки -parent: Руководство пользователя nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Просматривайте расположение нод на карте, создавайте и делитесь путевыми точками, управляйте слоями карты и планировщиком участков, а также контролируйте передачу геоданных и приватность. aliases: - map @@ -102,7 +101,7 @@ Waypoints always broadcast to the whole mesh on the primary channel. Unlike a me ## Слои карты -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Импортированные слои отображаются в списке с переключателем для показа/скрытия каждого и возможностью удалить слой. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/ru-rRU/user/messages-and-channels.md b/docs/ru-rRU/user/messages-and-channels.md index c99c234384..9d568afbde 100644 --- a/docs/ru-rRU/user/messages-and-channels.md +++ b/docs/ru-rRU/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Сообщения и каналы -parent: Руководство пользователя nav_order: 3 last_updated: 2026-09-14 description: Отправляйте и получайте сообщения, управляйте каналами, настраивайте шифрование, ищите по перепискам, а также используйте быстрый чат, реакции и действия с сообщениями. diff --git a/docs/ru-rRU/user/mqtt.md b/docs/ru-rRU/user/mqtt.md index aad9337632..3987f7a4dc 100644 --- a/docs/ru-rRU/user/mqtt.md +++ b/docs/ru-rRU/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: Руководство пользователя nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Подключите свою mesh-сеть к интернету — настройка MQTT-брокера, уровни шифрования и отчётность на карте. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Отключено | | **TLS enabled** | Безопасное подключение к брокеру | Отключено | | **Map reporting** | Сообщать о местоположении на публичную карту | Отключено | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Включено | +| **Proxy to client enabled** | Relay MQTT through the connected app | Включено | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT-прокси на этом телефоне +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Стандартный брокер Meshtastic Сообщество поддерживает публичный брокер по адресу `mqtt.meshtastic.org`. Он предназначен для общего использования и тестирования. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Приватность:** Сообщения на публичном брокере доступны для чтения всем, кто подписан. Всегда используйте шифрование каналов для конфиденциальной связи. @@ -93,7 +90,7 @@ When this phone relays MQTT for the radio, connections to that broker always use When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/ru-rRU/user/node-metrics.md b/docs/ru-rRU/user/node-metrics.md index 6abcf809cb..c85e95fd3a 100644 --- a/docs/ru-rRU/user/node-metrics.md +++ b/docs/ru-rRU/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Метрики ноды -parent: Руководство пользователя nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Панели телеметрии для каждой ноды mesh-сети — состояние устройства, датчики окружающей среды, качество воздуха, качество сигнала, питание, трассировка и история местоположения. aliases: - metrics @@ -20,15 +19,15 @@ aliases: 1. Перейдите в раздел **Ноды**. 2. Нажмите на ноду, которую хотите просмотреть. 3. Scroll to the **Telemetry** section and find the category you want — **Signal Quality**, **Device Metrics**, **Environment Metrics**, **Air-Quality Metrics**, **Power Metrics**, **Position**, and the rest. -4. Tap the refresh button on a row to ask the node for a fresh reading. The chart button beside it opens that category's history, and appears once the node has reported that kind of telemetry. +4. Нажми кнопку обновления в строке, чтобы получить от ноды свежие данные. Кнопка с графиком рядом с ним открывает историю по этой категории и появляется после того, как узел отправит данные такого рода. ![Сведения о ноде — локальное устройство](../../assets/screenshots/nodes_detail_local.png) -The **Position** row expands to show location data for nodes that share GPS: +Строка **Позиция** расширяется, чтобы отобразить данные о местоположении нод, использующих GPS. ![Встроенное содержимое о местоположении](../../assets/screenshots/nodes_position.png) -> ℹ️ **Note:** Metrics are only available when they have been reported by the remote node. Метрики обновляются с интервалами, настроенными в параметрах телеметрии каждой ноды. +> ℹ️ **Примечание.** Метрики доступны только после получения данных от удаленной ноды. Метрики обновляются с интервалами, настроенными в параметрах телеметрии каждой ноды. ## Интервал передачи @@ -38,13 +37,13 @@ The **Position** row expands to show location data for nodes that share GPS: | -------------- | -------------------------------------------------------- | | Уровень заряда | Текущий процент заряда батареи | | Напряжение | Показания напряжения батареи | -| ChUtil | Percentage of local airtime in use | +| ChUtil | Процент использованного местного эфирного времени | | AirUtil | Percentage of the last hour this node spent transmitting | | Аптайм | Время с момента последней перезагрузки | Device Metrics has no cards on the node detail screen. Use the chart button on its row to open the Device Metrics screen, where battery level, voltage, ChUtil, and AirUtil are plotted over time and every reading — uptime included — is listed with its timestamp underneath. Pick a time frame at the top of the screen, and use the save icon in the app bar to export the visible history as CSV. -> 💡 **Tip:** Where a category does show cards — Environment, Air Quality, and Power — touch & hold a card to copy its value to the clipboard. On a chart screen, pinch to zoom the time axis. +> 💡 **Tip:** Where a category does show cards — Environment, Air Quality, and Power — touch & hold a card to copy its value to the clipboard. На экране с диаграммой для масштабирования оси времени используй жест «щипок». ## Метрики окружения @@ -114,7 +113,7 @@ CO₂ readings are color-coded by severity so you can read air quality at a glan ### Оценка качества сигнала -Качество сигнала оценивается по **SNR относительно минимального уровня демодуляции активного пресета LoRa-модема**, а не по фиксированным порогам — конкретное значение SNR означает разное на разных пресетах (например, −15 дБ нормально для LongSlow, но неприемлемо для ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. +Качество сигнала оценивается по **SNR относительно минимального уровня демодуляции активного пресета LoRa-модема**, а не по фиксированным порогам — конкретное значение SNR означает разное на разных пресетах (например, −15 дБ нормально для LongSlow, но неприемлемо для ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. В противном случае RSSI отображается только на экране. In the table, _limit_ is the preset's SNR limit. | Качество | Критерии | | ----------- | -------------------------------- | @@ -152,7 +151,7 @@ The node detail screen shows cards for channels 1 to 3. Use the chart button on ### Чтение результатов трассировки -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/ru-rRU/user/nodes.md b/docs/ru-rRU/user/nodes.md index 9e342c77fc..49a41230f5 100644 --- a/docs/ru-rRU/user/nodes.md +++ b/docs/ru-rRU/user/nodes.md @@ -1,8 +1,7 @@ --- title: Ноды -parent: Руководство пользователя nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Просматривайте, фильтруйте и сортируйте ноды сети — просматривайте подробности, качество сигнала, роли и быстрые действия. aliases: - node-list @@ -15,30 +14,34 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Список узлов +## Node list -Список нод показывает все ноды, которые услышало твоё радиоустройство, включая: +The node list shows every node your node has heard, including: - **Имя ноды** — длинное имя, настроенное пользователем - **Короткое имя** — 4-символьный идентификатор -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Последнее услышанное** — время с последнего общения - **Расстояние** — предполагаемое расстояние (если позиции общие) - **Батарея** — уровень заряда батареи удалённой ноды (если включена телеметрия) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Индикаторы состояния ноды +### Node Status indicators -| Индикатор | Значение | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Нода слышна за последние 2 часа | -| Plain last-heard time | Нода не отвечала больше 2 часов | -| ⭐ Избранный | Node you marked as a favorite. | +| Индикатор | Значение | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Нода слышна за последние 2 часа | +| Plain last-heard time | Нода не отвечала больше 2 часов | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Избранный | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. ### Роли ноды @@ -58,18 +61,18 @@ There is no separate "away" tier. | Sensor | Оптимизировано для данных телеметрии | | Тактический | Взаимодействует с системами TAK (отправляет/принимает CoT) | | TAK Tracker | Только отчет о позиции TAK | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Выбор роли +### Choosing a role Большинству пользователей стоит оставить роль **Client** по умолчанию. Рассмотри другую роль, когда: -- **Router** — У тебя есть узел в фиксированном, высоком месте с надежным источником питания (крыша, вершина холма). Router постоянно бодрствуют, чтобы пересылать сообщения для других, и они необходимы для расширения покрытия сети. Don't use Router on battery-powered handheld radios. +- **Router** — У тебя есть узел в фиксированном, высоком месте с надежным источником питания (крыша, вершина холма). Router постоянно бодрствуют, чтобы пересылать сообщения для других, и они необходимы для расширения покрытия сети. Don't use Router on battery-powered handheld nodes. - **Router Late** — инфраструктурная нода, которая всегда пересылает пакеты только один раз, но только после того, как все другие режимы маршрутизации выполнили свои ходы. Обеспечивает дополнительное покрытие для локальных кластеров, не конкурируя с основными роутерами. - **Client Base** — обрабатывает трафик от/к вашим избранным нодам с приоритетом Router Late (обеспечивая этим сообщениям дополнительное ретранслирование), а всё остальное обрабатывает как обычный Client. -- **Client Mute** — хочешь принимать трафик сети, но не участвовать в его ретрансляции. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Спит между передачами для экономии батареи. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Похожий профиль мощности на Tracker. +- **Client Mute** — хочешь принимать трафик сети, но не участвовать в его ретрансляции. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Спит между передачами для экономии батареи. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Похожий профиль мощности на Tracker. - **TAK / TAK Tracker** — нужно только если работать с системами ATAK/WinTAK. Смотри [Интеграция TAK](tak) для подробностей. > 💡 **Совет:** Сеть работает лучше, когда большинство нод **Client** или **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. Хорошее практическое правило: один роутер на 5–10 клиентов в вашей зоне. @@ -78,19 +81,19 @@ There is no separate "away" tier. Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Значок | Значение | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Несовпадение | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Значок | Значение | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Несовпадение | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Быстрые действия @@ -107,10 +110,10 @@ A mismatch never replaces the key you already hold. The app keeps the first key Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Фильтрация и сортировка +## Filtering & sorting -### Поиск текста +### Text search Введи в поле поиска, чтобы отфильтровать ноды по имени или короткому имени. Фильтр обновляется в реальном времени по мере набора текста. -### Переключатели фильтра +### Filter toggles -| Фильтр | Описание | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Показывать только ноды, услышанные за последние 2 часа | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Включить неизвестные** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Исключить инфраструктуру** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Исключить MQTT** | Скрыть ноды, слышимые только через интернет-мост MQTT | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фильтр | Описание | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Показывать только ноды, услышанные за последние 2 часа | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Включить неизвестные** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Исключить инфраструктуру** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Исключить MQTT** | Скрыть ноды, слышимые только через интернет-мост MQTT | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Параметры сортировки +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Сортировка | Описание | | --------------------------------------------- | --------------------------------------------------------------------- | @@ -146,14 +167,14 @@ To add someone else's contact, use the import button on the node list and choose | **Расстояние** | Сначала ближайшие ноды (требуется обмен позициями) | | **Меньше хопов** | Сначала с наименьшим количеством ретрансляций | | **Канал** | Группировать по индексу канала | -| **via MQTT** | Сгруппировать по MQTT и радиоприему | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Нод на хоп +## Nodes per hop -Нажми на значок гистограммы хопов в панели приложений списка нод, чтобы открыть столбчатую диаграмму того, сколько нод находится на каждом расстоянии хопа (0 = напрямую, 1 = через один ретранслятор и так далее). Отфильтруй график по окну **последнего услышанного** — за всё время, 1 час, 8 часов или 24 часа — чтобы посмотреть, как сейчас выглядит сеть по сравнению с более длительным периодом. Это быстрый способ понять, насколько занята и разветвлена твоя местная сеть. +Нажми на значок гистограммы хопов в панели приложений списка нод, чтобы открыть столбчатую диаграмму того, сколько нод находится на каждом расстоянии хопа (0 = напрямую, 1 = через один ретранслятор и так далее). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. Это быстрый способ понять, насколько занята и разветвлена твоя местная сеть. -## Детали ноды +## Node detail Нажатие на ноду открывает подробный вид с полной информацией. Смотри [Метрики ноды](node-metrics) для полной информации о метриках и телеметрии. @@ -173,7 +194,19 @@ The Details card carries the node's short name, role, IDs, last heard time, hops | Последний раз слышен | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Расстояние | ![Расстояние](../../assets/screenshots/nodes_distance_info.png) | -### Ссылки на устройства ("Хочу такое") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Значение | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") Когда оборудование ноды распознано, в детальном просмотре появляется сворачиваемый раздел **"Хочу такой"**, содержащий ссылки на места, где можно купить или узнать больше об этом устройстве: страницу продукта у производителя, варианты продукта и объявления на региональных торговых площадках (например, AliExpress, Amazon и у поддерживаемых продавцов), отфильтрованные по твоей стране. Каждая ссылка открывается через сервис перенаправления `msh.to`. Устройства без подходящих ссылок не показывают этот раздел. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Связанные темы diff --git a/docs/ru-rRU/user/notifications.md b/docs/ru-rRU/user/notifications.md new file mode 100644 index 0000000000..4fa559b128 --- /dev/null +++ b/docs/ru-rRU/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Уведомления +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Уведомления + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ---------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Сообщения | Уведомления о личных сообщениях | A message sent directly to you | The conversation | +| Сообщения | Уведомления о сообщениях в общем чате | A message on one of your channels | The channel | +| Сообщения | Уведомления о путевых точках | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Сообщения | Служебные уведомления | A critical alert from a node | The conversation | +| Меш | Уведомления о новых нодах | A node heard for the first time | The node's details | +| Меш | Уведомления о приглашениях в сеть | An invitation to join a nearby mesh | Локальное обнаружение сети | +| Меш | Уведомления о низком заряде батареи (избранные ноды) | A favorite node's battery running low | The node's details | +| Устройство | Служебные уведомления | The connection to your node while the app runs in the background | The app | +| Устройство | Уведомление о низком уровне заряда | Your node's battery running low | The node's details | +| Устройство | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Устройство | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Связанные темы + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/ru-rRU/user/onboarding.md b/docs/ru-rRU/user/onboarding.md index 6a51015843..3ab6a4a3f1 100644 --- a/docs/ru-rRU/user/onboarding.md +++ b/docs/ru-rRU/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Начало работы -parent: Руководство пользователя nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: Настройка при первом запуске — разрешения, процесс знакомства с приложением и следующие шаги после подключения твоей радиостанции. aliases: - first-launch @@ -55,9 +54,11 @@ Meshtastic также использует местоположение для: - Вычисление расстояний до других нод - Обмен GPS-координатами с другими участниками сети (если включено) -Предоставьте **"При использовании приложения"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. -Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. +Предоставьте **"При использовании приложения"**. Приложение не запрашивает фоновое определение местоположения — в его манифесте нет параметра `ACCESS_BACKGROUND_LOCATION` — поэтому Android не предлагает опцию "Всегда", и обновление местоположения происходит, когда приложение находится на переднем плане или выполняет функцию службы переднего плана. + +При отключении остальная часть приложения продолжает работать: на Android 12 и более новых версиях Bluetooth не затрагивается, отключаются только отображение местоположения на карте и обмен данными о местоположении. На Android 11 и более ранних версиях сканирование Bluetooth также прекращается, поскольку для этого требуется разрешение, которое Android блокирует. Кроме того, для получения каких-либо результатов сканирования необходимо включить системные **службы определения местоположения**. ### Разрешение на уведомления @@ -71,25 +72,25 @@ Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth ### Разрешение на критические уведомления -Critical alerts are high-priority notifications that break through Do Not Disturb — for emergency mesh alerts and urgent messages. +Критические оповещения — это уведомления с высоким приоритетом, которые проходят через режим «Не беспокоить» и предназначены для экстренных вызовов и срочных сообщений. -This step is not a runtime permission prompt. There is no grant/deny dialog: the button opens the Android system settings page for the app's **Alerts** notification channel, where you turn the breakthrough behavior on yourself. Tap **Configure Critical Alerts** to open that page, or **Skip** to move on — you can reach the same page later from Android's notification settings for Meshtastic. This step appears only if you granted notifications on the previous screen — skip or decline them and setup ends there. +Этот шаг не является запросом на разрешение запуска. There is no grant/deny dialog: the button opens the Android system settings page for the app's **Alerts** notification channel, where you turn the breakthrough behavior on yourself. Tap **Configure Critical Alerts** to open that page, or **Skip** to move on — you can reach the same page later from Android's notification settings for Meshtastic. This step appears only if you granted notifications on the previous screen — skip or decline them and setup ends there. -### Reviewing permissions later +### Пересмотреть разрешения позже The **Permissions** section of **Settings** summarizes where every runtime permission stands. On Android 12 and newer it lists five: **Nearby devices permission** (Bluetooth), **Location permission**, **App Notifications**, **Camera permission** (scanning channel and contact QR codes) and **Local network permission** (finding radios over Wi-Fi by mDNS). On Android 11 and older a single **Location permission** row covers both Bluetooth and location, so there are four. The last two are never asked for during setup, only when a feature first needs them. The section reads _All allowed_ when every permission is granted, _Nothing needs your attention_ when some have simply never been asked for, and names a count when one is denied — in which case it expands itself. Tap the row to expand or collapse it at any time: -| Состояние | What tapping the row does | -| ------------------------------------------- | -------------------------------------------------------------------------------------------- | -| **Allowed** | Opens the system page, so you can review or revoke it | -| **Not asked yet** | Requests it | -| **Denied — tap to allow** | Explains what the permission is for, then asks again if you agree | -| **Blocked — tap to open system settings** | Android will no longer show its dialog, so this opens the page where you can turn it back on | -| **Not required on this version of Android** | Ничего — разрешения на твоём устройстве нет | +| Состояние | What tapping the row does | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| **Разрешено** | Открывает страницу системы, на которой можно просмотреть или отозвать запрос | +| **Ещё не спрашивали** | Запросить | +| **Отклонено — нажми для разрешения** | Объясняет, для чего нужно разрешение, а затем снова спрашивает, согласен ли ты. | +| **Заблокировано — нажми, чтобы открыть системные настройки** | Android больше не будет отображать диалоговое окно, поэтому откроется страница, на которой можно снова его включить | +| **Не требуется для данной версии Android** | Ничего — разрешения на твоём устройстве нет | -This matters most for notifications. If you decline them during setup, this row is the way back: Android stops showing the dialog once you have declined firmly (a second denial), at which point this row switches to **Blocked** and sends you to the system settings page instead. Подсказка уведомлений есть только на Android 13 и новее — на более старых версиях уведомления включены по умолчанию и ими управляют через настройки самого Android. +Это больше всего важно для уведомлений. Если вы отклонил их во время настройки, эта строка станет обратной: Android перестанет показывать диалоговое окно, как только ты решительно откажешься (повторный отказ), после чего эта строка переключится на \*\* Заблокировано \*\* и вместо этого отправит тебя на страницу настроек системы. Подсказка уведомлений есть только на Android 13 и новее — на более старых версиях уведомления включены по умолчанию и ими управляют через настройки самого Android. ## После настройки @@ -107,4 +108,4 @@ This matters most for notifications. If you decline them during setup, this row - [Сообщения и каналы](messages-and-channels) — отправь своё первое сообщение - [Ноды](nodes) — посмотри, кто ещё в твоей сети - [Карта и контрольные точки](map-and-waypoints) — просмотр позиций нод -- [Settings — Radio & User](settings-radio-user) — configure your radio and user profile +- [Настройки — Радио и пользователь](settings-radio-user) — настрой радио и свой профиль пользователя diff --git a/docs/ru-rRU/user/settings-module-admin.md b/docs/ru-rRU/user/settings-module-admin.md index 4517ff4a84..80d8852359 100644 --- a/docs/ru-rRU/user/settings-module-admin.md +++ b/docs/ru-rRU/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Настройки — Модули и администрирование -parent: Руководство пользователя nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Настрой дополнительные функциональные модули (MQTT, телеметрия, готовые сообщения, TAK и другие) и выполняй администрирование устройств. aliases: - modules @@ -14,7 +13,7 @@ aliases: Настрой дополнительные функциональные модули и выполняй управление устройством. Модули расширяют Meshtastic с помощью специализированных возможностей — каждый из них можно включать или отключать отдельно. -> 💡 **Совет:** Тебе нужно включать только те модули, которые действительно используешь. Отключение неиспользуемых модулей снижает время передачи, экономит батарею и упрощает конфигурацию. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Совет:** Тебе нужно включать только те модули, которые действительно используешь. Отключение неиспользуемых модулей снижает время передачи, экономит батарею и упрощает конфигурацию. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Настройки модулей используют макет на основе карточек с переключателями, выпадающими списками, текстовыми полями и ползунками: @@ -26,43 +25,43 @@ aliases: ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Конфигурация модуля +## Настройки модуля Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### Модуль MQTT +### MQTT module -Мосты передают сообщения туда и обратно от брокера MQTT для подключения к интернету. Ты так расширишь сеть за пределы радиуса действия или интегрируешь её с системами домашней автоматизации. +Мосты передают сообщения туда и обратно от брокера MQTT для подключения к интернету. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Настройка | Описание | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT включен | Переключить MQTT мост | -| Адрес | Адрес MQTT брокера | -| Имя пользователя | Имя пользователя для аутентификации | -| Пароль | Пароль аутентификации | -| Шифрование включено | Зашифровать MQTT-пейлоады | -| Вывод JSON включен | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS включен | Использовать защищённое соединение | -| Корневая тема | Базовый путь темы MQTT | -| Прокси клиенту включен | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT-прокси на этом телефоне | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Отчёты по карте | Publish position to the public map — see the Map reporting group that follows | +| Настройка | Описание | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT включен | Переключить MQTT мост | +| Адрес | Адрес MQTT брокера | +| Имя пользователя | Имя пользователя для аутентификации | +| Пароль | Пароль аутентификации | +| Шифрование включено | Зашифровать MQTT-пейлоады | +| Вывод JSON включен | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS включен | Использовать защищённое соединение | +| Корневая тема | Базовый путь темы MQTT | +| Прокси клиенту включен | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Отчёты по карте | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Настройка | Описание | -| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Я согласен. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Интервал отчета карты (в секундах) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Настройка | Описание | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Я согласен. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | См. [MQTT](mqtt) для подробного руководства по использованию, включая шифрование, конфиденциальность и настройку брокера. -### Последовательный модуль +### Serial module Позволяет общаться через последовательный порт с внешними устройствами (GPS-модулями, датчиками или собственной техникой). Когда включено, последовательный порт ноды может отправлять и получать данные в формате protobuf или текст, что позволяет внешним микроконтроллерам или компьютерам взаимодействовать с сетью. @@ -76,29 +75,29 @@ until you agree: | Время ожидания истекло | How long to wait before considering an incoming message complete | | Переопределить COM-порт консоли | Take over the port the debug console normally uses | -### Модуль внешних уведомлений +### External Notification module -Управляет зуммером, светодиодом или вибрацией на вашем радиооборудовании. Полезно для устройств, которым нужно физически сигнализировать о приходе сообщения — особенно удобно для неоснащенных персоналом или уличных установок. +Controls buzzer, LED, or vibration alerts on your node hardware. Полезно для устройств, которым нужно физически сигнализировать о приходе сообщения — особенно удобно для неоснащенных персоналом или уличных установок. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Настройка | Описание | -| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Внешние уведомления включены | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Выход LED (GPIO) | Pin the LED is wired to | -| Вывод светодиода активный высокий | Whether the LED pin is active high or low | -| Выход Буззера (GPIO) | Pin the buzzer is wired to | -| Вибросигнал (GPIO) | Pin the vibration motor is wired to | -| Использовать PWM-звукоизлучатель | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Использовать I2S как буззер | Send the alert through an I2S audio output instead | -| Продолжительность вывода (миллисекунды) | How long a single alert lasts | -| Таймаут Nag (в секундах) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Рингтон | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Настройка | Описание | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Внешние уведомления включены | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Выход LED (GPIO) | Pin the LED is wired to | +| Вывод светодиода активный высокий | Whether the LED pin is active high or low | +| Выход Буззера (GPIO) | Pin the buzzer is wired to | +| Вибросигнал (GPIO) | Pin the vibration motor is wired to | +| Использовать PWM-звукоизлучатель | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Использовать I2S как буззер | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Интервал повтора | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Рингтон | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Модуль Store & Forward +### Store & Forward module Буферизирует сообщения для узлов, которые временно были недоступны, а затем ретранслирует их, когда эти узлы переподключаются. Важное значение для сеток, где узлы входят и выходят вне диапазона регулярно - обеспечивает отсутствие потери сообщений при коротких разъединениях. @@ -113,7 +112,7 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin > 💡 **Совет:** Хранение и пересылка лучше всего работает на узлах с достаточной памятью (ESP32 с PSRAM). Узлы маршрутизатора являются идеальными кандидатами, так как они обычно всегда включены. -### Модуль проверки дальности +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin Автоматизированный инструмент для проверки дальности и оценки качества связи между нодами. Когда включено, нода периодически отправляет сообщения с увеличивающимся счетчиком. Приёмная нода записывает эти сообщения, что позволяет тебе уйти пешком или уехать на машине, а потом проанализировать, на каком расстоянии сообщения перестали приходить. -| Настройка | Описание | -| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Проверка дальности включена | Активировать проверку дальности | -| Интервал сообщений отправителя (в секундах) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Сохранить .CSV в хранилище (только ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Настройка | Описание | +| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Проверка дальности включена | Активировать проверку дальности | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Сохранить .CSV в хранилище (только ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Модуль телеметрии +### Telemetry module Контролирует какими телеметрическими данными ваш узел делится с сеткой. Телеметрия включает данные о состоянии устройства (заряд батареи, время работы) и данные с датчиков окружающей среды (температура, влажность, давление). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Настройка | Описание | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Отправлять телеметрию устройства | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Интервал обновления метрик устройства | How often to report battery, uptime and channel utilization | -| Модуль метрик окружения включен | Report the attached environment sensors | -| Интервал обновления метрик среды | How often to report them | -| Показатели окружения на экране включены | Also show these readings on the device's own display | -| Использовать метрику окружения в Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Модуль измерения качества воздуха включен | Report particulate and CO₂ sensor data | -| Интервал обновления данных качества воздуха | How often to report them | -| Модуль метрик питания включен | Report the per-channel voltage and current readings | -| Интервал обновления метрик электропитания | How often to report them | -| Включить метрики питания на экране | Also show power readings on the device's display | +| Настройка | Описание | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Отправлять телеметрию устройства | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Интервал обновления метрик устройства | How often to report battery, uptime and channel utilization | +| Модуль метрик окружения включен | Report the attached environment sensors | +| Интервал обновления метрик среды | How often to report them | +| Показатели окружения на экране включены | Also show these readings on the device's own display | +| Использовать метрику окружения в Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Модуль измерения качества воздуха включен | Report particulate and CO₂ sensor data | +| Интервал обновления данных качества воздуха | How often to report them | +| Модуль метрик питания включен | Report the per-channel voltage and current readings | +| Интервал обновления метрик электропитания | How often to report them | +| Включить метрики питания на экране | Also show power readings on the device's display | Посмотрите [Телеметрия и датчики](telemetry-and-sensors) — для получения информации о поддерживаемых датчиках и рекомендациях по настройке. -### Модуль шаблонных сообщений +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Определите список быстрых сообщений, которые могут быть переданы без подключённого телефона — идеально подходит для использования в поле. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Определите список быстрых сообщений, которые могут быть переданы без подключённого телефона — идеально подходит для использования в поле. | Настройка | Описание | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Вверх/Вниз/Выбирать включён | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Звуковой модуль +### Audio module Поддержка аудио Codec2 для низкополосной голосовой связи через сетку. Это **экспериментальная функция**, которая кодирует голос в очень маленькие пакеты данных с помощью кодека Codec2. @@ -182,11 +181,11 @@ Pre-configured messages accessible from the radio's physical buttons (for radios > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Качество голоса очень низкополосное — представьте себе «разборчивую радиосвязь», а не качество телефонного звонка. -### Удаленный аппаратный модуль +### Remote Hardware module Управление GPIO через mesh-сеть. Позволяет удалённому узлу читать и записывать состояния выводов GPIO на другом узле — полезно для активации реле, опроса переключателей или удалённого управления внешним оборудованием. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Настройка | Описание | | -------------------------------- | ------------------------------------------------------------------------------- | @@ -194,19 +193,19 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Разрешить неопределённый контакт | Разрешить доступ к любому GPIO-пину (риск для безопасности) | | Доступные контакты | До 4 пинов GPIO, которые этот узел предоставляет для удалённого чтения и записи | -### Модуль информации о соседях +### Neighbor Info module Транслирует информацию о доступных услышанных соседей, включив ячейку сеточной топологии. Каждый включенный узел периодически делится списком других узлов которые он может слышать и их качество сигнала. -| Настройка | Описание | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| Информация о соседях включена | Включить трансляцию соседей | -| Интервал обновления (в секундах) | Как часто транслировать список соседей | -| Передать через LoRa | Также транслировать информацию соседей по LoRa, а не только MQTT/телефон. Недоступно на канале используя ключ по умолчанию и имя | +| Настройка | Описание | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Информация о соседях включена | Включить трансляцию соседей | +| Интервал опроса GPS | Как часто транслировать список соседей | +| Передать через LoRa | Также транслировать информацию соседей по LoRa, а не только MQTT/телефон. Недоступно на канале используя ключ по умолчанию и имя | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Модуль окружающего освещения +### Ambient Lighting module Управляет встроенными светодиодами NeoPixel или другими адресуемыми RGB-светодиодами на поддерживаемом оборудовании. Может использоваться для визуальных статусовых индикаторов, световых уведомлений, или декоративных эффектов. @@ -216,49 +215,49 @@ See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topo | Ток | Текущий лимит светодиодов (0–31) | | Красный / Зеленый / Синий | Индивидуальные значения цветов канала (0–255) | -### Модуль определения датчика +### Detection Sensor module Превращает ваш узел в систему сигнализации на основе датчика движения или открытия двери. При обнаружении изменения состояния на выводе GPIO (например, сработал датчик движения или открылась дверь) узел отправляет по меш-сети оповещение. -| Настройка | Описание | -| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| Датчик определения включен | Активировать датчик обнаружения | -| GPIO контакт для мониторинга | Пин GPIO, подключенный к датчику | -| Тип триггера обнаружения | Как состояние пина интерпретируется как событие обнаружения (например, активный высокий/низкий уровень, срабатывание по фронту) | -| Использовать режим INPUT_PULLUP | Включить внутренний подтягивающий резистор пина | -| Минимальная трансляция (в секундах) | Минимальный интервал между оповещениями | -| Трансляция состояния (в секундах) | Интервал периодической отправки состояния | -| Отправить колокол с уведомлением | Включать символ колокола в оповещения | -| Понятное имя | Пользовательское имя для этого датчика | +| Настройка | Описание | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Датчик определения включен | Активировать датчик обнаружения | +| GPIO контакт для мониторинга | Пин GPIO, подключенный к датчику | +| Тип триггера обнаружения | Как состояние пина интерпретируется как событие обнаружения (например, активный высокий/низкий уровень, срабатывание по фронту) | +| Использовать режим INPUT_PULLUP | Включить внутренний подтягивающий резистор пина | +| Minimum time between detection broadcasts | Минимальный интервал между оповещениями | +| State Broadcast Interval | Интервал периодической отправки состояния | +| Отправить колокол с уведомлением | Включать символ колокола в оповещения | +| Понятное имя | Пользовательское имя для этого датчика | -### Paxcounter Модуль +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Засчитывает ближайшие устройства, пассивно прослушивая зондирующие запросы, чтобы телефоны и ноутбуки излучали при сканировании сетей. Доступно только на устройствах ESP32. -| Настройка | Описание | -| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter включен | Активировать подсчет людей | -| Интервал обновления (в секундах) | Как часто сообщать подсчитывания | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Настройка | Описание | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter включен | Активировать подсчет людей | +| Интервал опроса GPS | Как часто сообщать подсчитывания | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Совет:** Paxcounter полезен для приблизительной оценки пешеходного потока в местах начала маршрутов, на мероприятийных площадках или в других локациях. Счетчики приблизительны — один человек может иметь несколько устройств. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### Модуль TAK +### TAK module Интеграция Team Awareness Kit для совместимости с ATAK и WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. См. [TAK Integration](tak) для детальной настройки и использования. @@ -279,32 +278,33 @@ true before the entry appears in the module list: the radio runs firmware 2.8.0 **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Действие | Что она делает | -| --------------------------- | ------------------------------------------------------------------------------------------------------ | -| Установить время | Sends your phone's clock to the radio | -| Перезагрузка | Restarts the radio | -| Выключение | Powers the radio down | -| Сброс до заводских настроек | Returns every setting to its factory default | -| Очистка списка нод сети | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Действие | Что она делает | +| --------------------------- | ---------------------------------------------------------------------------------------------- | +| Установить время | Sends your phone's clock to the node | +| Перезагрузка | Restarts the node | +| Выключение | Powers the node down | +| Сброс до заводских настроек | Returns every setting to its factory default | +| Очистка списка нод сети | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Бэкап & Восстановление -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Расширенные **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Очистить базу данных нод -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ say — that is why the queued list can be shorter than you expect. ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### О приложении @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Устранение неполадок удалённого администрирования +### Troubleshooting remote admin - **"Нет ответа от целевого узла"** — цель может находиться вне диапазона, в автономном режиме или иметь несоответствующий ключ администратора. Проверьте соответствие ключа администратора на обоих узлах. - **Изменения не применены** — чтобы некоторые настройки вступили в силу, нужно перезагрузить устройство. Попробуй перезагрузить после сохранения. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Связанные темы -- [Настройки — Радио и Пользователь](settings-radio-user) — основные настройки радио и профиля пользователя +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Ссылка на конфигурацию модуля](https://meshtastic.org/docs/configuration/module) — подробная документация по модулям на meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — общие вопросы на meshtastic.org diff --git a/docs/ru-rRU/user/settings-radio-user.md b/docs/ru-rRU/user/settings-radio-user.md index bc90da8ac4..3ba16f2dad 100644 --- a/docs/ru-rRU/user/settings-radio-user.md +++ b/docs/ru-rRU/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Настройки - Радио и пользователь -parent: Руководство пользователя nav_order: 7 -last_updated: 2026-09-11 -description: Настройте ваше радиоустройство, пресеты LoRa, пользовательский профиль, обмен местоположением, управление питанием и безопасность. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - настройки - radio-config @@ -13,14 +12,14 @@ aliases: # Настройки - Радио и пользователь -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Настройки используют стандартные элементы управления предпочтениями — выпадающие списки, переключатели и ползунки: @@ -36,20 +35,20 @@ only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Blu On **Settings → User**. -| Настройка | Описание | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Полное имя | Ваше отображаемое имя (до 39 символов) | -| Короткое имя | 4-символьное сокращённое имя | -| Состояние сообщения | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Без сообщений | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Лицензия радиолюбителя (HAM) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Настройка | Описание | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Полное имя | Ваше отображаемое имя (до 39 символов) | +| Короткое имя | 4-символьное сокращённое имя | +| Состояние сообщения | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Без сообщений | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Применение изменений +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Настройка | Описание | По умолчанию | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| Регион / Страна | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Не установлено (необходимо настроить) | -| Шаблоны | Компромисс между скоростью и дальностью | LongFast | -| Количество прыжков | Максимальное количество ретрансляций | 3 | -| Мощность передатчика | Мощность передачи (дБм); 0 = максимально разрешённая для региона | 0 (максимум региона) | -| Переопределить частоту | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Использовать шаблон | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Коэффициент распространения | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Частота кодирования | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Ширина канала | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Частота слота | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Передача включена | Turning this off makes the node receive-only | On | -| Переопределить рабочий цикл | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Выкл | -| Игнорировать MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| ОК в MQTT | Allow your packets to be forwarded to MQTT by gateways | Выкл | -| Усиление RX | Extra receive gain on SX126x radios; costs a little current | Выкл | -| PA вентилятор выключен | Turn off the power-amplifier fan on hardware that has one | Выкл | +| Настройка | Описание | По умолчанию | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| Регион / Страна | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Не установлено (необходимо настроить) | +| Шаблоны | Компромисс между скоростью и дальностью | LongFast | +| Количество прыжков | Максимальное количество ретрансляций | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (максимум региона) | +| Переопределить частоту | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Использовать шаблон | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Коэффициент распространения | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Частота кодирования | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Ширина канала | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Частота слота | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Передача включена | Turning this off makes the node receive-only | On | +| Переопределить рабочий цикл | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Выкл | +| Игнорировать MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| ОК в MQTT | Allow your packets to be forwarded to MQTT by gateways | Выкл | +| Усиление RX | Extra receive gain on SX126x radios; costs a little current | Выкл | +| PA вентилятор выключен | Turn off the power-amplifier fan on hardware that has one | Выкл | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. Смотрите [руководство по настройке региона](https://meshtastic.org/docs/getting-started/initial-config) на сайте meshtastic.org для получения подробной информации. -### Предустановки модема +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Совет:** Значения **порога SNR** специально отрицательные. LoRa может декодировать сигналы _ниже_ уровня шума, поэтому более отрицательный предел означает, что пресет допускает более слабый, шумный сигнал (больший радиус действия). Смотрите [Как работает измеритель сигнала](signal-meter) для полного объяснения. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 км | 1.30 kbps | -10 дБ | Диапазон RU 868 МГц (ширина полосы 62,5 кГц); аналогично Long Fast | | Medium Turbo | ~5 км | 7.0 kbps | −12.5 дБ | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 км | 0.68 kbps | −7.5 дБ | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 км | 0.33 kbps | -10 дБ | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 км | 0.33 kbps | -10 дБ | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 км | 0.18 кб/с | −20 дБ | ⚠️ **Устарел** — всё ещё доступен для выбора, но может быть удален в будущих версиях прошивки | | ~~Very Long Slow~~ | ~40+ км | 0.09 кб/с | −20 дБ | ⚠️ **Устарел** — всё ещё доступен для выбора, но может быть удален в будущих версиях прошивки | > i **Примечание:** В этой таблице используются общие короткие имена. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Выбор предустановки модема +#### Choosing a modem preset Предустановка модема управляет основным компромиссом между **дальностью** и **скоростью передачи данных**: @@ -141,38 +147,36 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — - **Фиксированные инфраструктурные связи:** Используй **Short Turbo** или **Long Turbo** для выделенных соединений точка-точка с хорошими антеннами и прямой видимостью. - **Смешанные среды:** Используй **Long Fast** — это настройка по умолчанию в сообществе и она обеспечит совместимость с другими в вашем регионе. -All nodes on the same channel must use the same modem preset. Ноды с несовпадающими предустановками не смогут обмениваться данными, даже если они используют одну частоту и ключ шифрования. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Преимущество высоты (вершина холма, крыша) значительно увеличивает эффективную дальность. Хорошо размещённый маршрутизатор с Long Fast часто может превзойти наземную ноду с Long Slow. ### Параметры дисплея -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Настройка | Описание | -| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Включать экран на | How long the display stays lit before sleeping | -| Интервал карусели | How often the radio cycles between screens on its own | -| Режим экрана | Screen layout/density used by the firmware | -| Система измерения | Metric or Imperial on the radio's screen | -| Использовать 12-часовой формат времени | Show the radio's clock as 12-hour rather than 24-hour | -| Выделять заголовок жирным | Draw the screen's heading text in bold | -| Повернуть экран | Rotate the display 180° for an inverted mounting | -| Тип OLED | Авто, SSD1306, SH1106, SH1107 | -| Включать экран при касании или движении | Light the screen when the radio is tapped or moved | -| Направление компаса | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Всегда указывать на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Настройка | Описание | +| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Включать экран на | How long the display stays lit before sleeping | +| Интервал карусели | How often the node cycles between screens on its own | +| Режим экрана | Screen layout/density used by the firmware | +| Система измерения | Metric or Imperial on the node's screen | +| Использовать 12-часовой формат времени | Show the node's clock as 12-hour rather than 24-hour | +| Выделять заголовок жирным | Draw the screen's heading text in bold | +| Повернуть экран | Rotate the display 180° for an inverted mounting | +| Тип OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Включать экран при касании или движении | Light the screen when the node is tapped or moved | +| Направление компаса | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Всегда указывать на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Настройки местоположения On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Настройка | Описание | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Режим GPS (физическое оборудование) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Интервал опроса GPS | How often the radio asks its GPS for a fix | +| Интервал опроса GPS | How often the node asks its GPS for a fix | | Период рассылки | How often the position is shared with the mesh | | Умная позиция | Broadcast based on movement rather than purely on the clock | | Умный интервал | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Настройка | Описание | | ---------------------------------------------- | --------------------------------------------------------------- | -| Включить режим энергосбережения | Let the radio sleep aggressively between activity | +| Включить режим энергосбережения | Let the node sleep aggressively between activity | | Выключение при потере мощности | Power the device down after external power disappears | | Длительность супер-глубокого сна | How long the deepest sleep state lasts | -| Минимальное время бодрствования | The shortest time the radio stays awake once woken | +| Минимальное время бодрствования | The shortest time the node stays awake once woken | | Длительность ожидания Bluetooth | How long to wait for a phone to connect before sleeping | | Коэффициент переопределения ADC | Turn on a manual correction for battery-voltage readings | | Коэффициент переопределения ADC | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Настройка сети -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Настройка | Описание | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | Название сети | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Пароль | Пароль сети | | Ethernet включен | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Настройка Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Настройка | Описание | | --------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Настройка | Описание | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Публичный ключ | Публичный ключ твоей ноды (только для чтения) | | Ключ администратора | Keys permitted to administer this node remotely — up to three | -| Приватный ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Приватный ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Пересоздать приватный ключ | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Канал администратора включен~~ | ⚠️ Удалено — теперь настраивается автоматически при установке ключа администратора | | Консоль COM-порта | Serial console over the Stream API | -| API журнала отладки включен | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Управляемый режим | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API журнала отладки включен | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Управляемый режим | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Резервное копирование | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Восстановить ключи | Запишисать сохранённые ключи обратно на ноду (доступно, как только есть резервная копия) | | Удалить резервную копию ключа | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/ru-rRU/user/signal-meter.md b/docs/ru-rRU/user/signal-meter.md index b4c9592661..1df4fc0c5d 100644 --- a/docs/ru-rRU/user/signal-meter.md +++ b/docs/ru-rRU/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: Как работает измеритель сигнала Meshtastic -parent: Руководство пользователя nav_order: 15 last_updated: 2026-09-09 description: Как индикатор сигнала оценивает качество по SNR относительно пресетов модема LoRa — расширенная спектральная модуляция, предустановки и что на самом деле означают полоски. diff --git a/docs/ru-rRU/user/tak.md b/docs/ru-rRU/user/tak.md index d440f23a4b..6adcc87822 100644 --- a/docs/ru-rRU/user/tak.md +++ b/docs/ru-rRU/user/tak.md @@ -1,8 +1,7 @@ --- title: Интеграция TAK -parent: Руководство пользователя nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Взаимодействие с ATAK и WinTAK — передача данных CoT о местоположении, роли TAK и настройка плагина. aliases: - tak @@ -105,6 +104,7 @@ Meshtastic поддерживает два формата передачи да - Сообщения чата могут передаваться между mesh-сетью и сетями TAK - Обновления местоположения передаются двунаправленно между Meshtastic и TAK - Ноды TAK Tracker автоматически передают PLI — их координаты отображаются на картах ATAK без какой-либо настройки на стороне ATAK +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/ru-rRU/user/telemetry-and-sensors.md b/docs/ru-rRU/user/telemetry-and-sensors.md index d5f6f69931..3ea926d02f 100644 --- a/docs/ru-rRU/user/telemetry-and-sensors.md +++ b/docs/ru-rRU/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Телеметрия и датчики -parent: Руководство пользователя nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Данные датчиков в mesh-сети — поддерживаемые датчики окружающей среды, качества воздуха и питания, а также руководства по настройке и просмотру. aliases: - sensors @@ -43,11 +42,12 @@ aliases: ### Качество воздуха -| Sensor | Метрика | Заметки | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Сопротивление газа / IAQ | Летучие органические соединения | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Метрика | Заметки | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Сопротивление газа / IAQ | Летучие органические соединения | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ aliases: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Освещённость и УФ | Sensor | Метрика | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Метрическая | Единица | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Радиация | µR/h | Card and chart | -| Вес | kg or lb | Card only — load cells, such as a beehive scale | -| Расстояние | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Метрическая | Единица | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Радиация | µR/h | Card and chart | +| Вес | kg or lb | Card only — load cells, such as a beehive scale | +| Расстояние | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Метрики питания diff --git a/docs/ru-rRU/user/translate.md b/docs/ru-rRU/user/translate.md index bcaeeb1380..de49f5f480 100644 --- a/docs/ru-rRU/user/translate.md +++ b/docs/ru-rRU/user/translate.md @@ -1,6 +1,5 @@ --- title: Перевод приложения -parent: Руководство пользователя nav_order: 17 last_updated: 2026-09-11 description: Как приложение и его документация переводятся через Crowdin и рекомендации по внесению переводов. diff --git a/docs/ru-rRU/user/units-and-locale.md b/docs/ru-rRU/user/units-and-locale.md index cf94bc7020..5a025292fb 100644 --- a/docs/ru-rRU/user/units-and-locale.md +++ b/docs/ru-rRU/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Единицы измерения и локаль -parent: Руководство пользователя nav_order: 16 last_updated: 2026-08-30 description: Как приложение отображает температуру, расстояние, скорость и другие показатели в зависимости от настроек устройства. diff --git a/docs/ru-rRU/user/widget.md b/docs/ru-rRU/user/widget.md index 0fa75db8df..e56cf16b34 100644 --- a/docs/ru-rRU/user/widget.md +++ b/docs/ru-rRU/user/widget.md @@ -1,6 +1,5 @@ --- title: Виджет на главный экран -parent: Руководство пользователя nav_order: 20 last_updated: 2026-08-30 description: Добавь виджет главного экрана Meshtastic, чтобы видеть местную статистику своего подключенного радио без открытия приложения. diff --git a/docs/sk-rSK/user/app-functions.md b/docs/sk-rSK/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/sk-rSK/user/app-functions.md +++ b/docs/sk-rSK/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/sk-rSK/user/connections.md b/docs/sk-rSK/user/connections.md index da818ca813..d136b2e68b 100644 --- a/docs/sk-rSK/user/connections.md +++ b/docs/sk-rSK/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Sieť | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/sk-rSK/user/debug-logs.md b/docs/sk-rSK/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/sk-rSK/user/debug-logs.md +++ b/docs/sk-rSK/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/sk-rSK/user/desktop.md b/docs/sk-rSK/user/desktop.md index 90a4c7ac8c..367a6e5e46 100644 --- a/docs/sk-rSK/user/desktop.md +++ b/docs/sk-rSK/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/sk-rSK/user/discovery.md b/docs/sk-rSK/user/discovery.md index f5b81c4a21..674af3fb6e 100644 --- a/docs/sk-rSK/user/discovery.md +++ b/docs/sk-rSK/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Popis | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Popis | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Informácia o susedoch @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/sk-rSK/user/firmware.md b/docs/sk-rSK/user/firmware.md index 72d221976b..351e5cde31 100644 --- a/docs/sk-rSK/user/firmware.md +++ b/docs/sk-rSK/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/sk-rSK/user/help-and-docs.md b/docs/sk-rSK/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/sk-rSK/user/help-and-docs.md +++ b/docs/sk-rSK/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/sk-rSK/user/map-and-waypoints.md b/docs/sk-rSK/user/map-and-waypoints.md index a8d50f6b76..2d73f244e0 100644 --- a/docs/sk-rSK/user/map-and-waypoints.md +++ b/docs/sk-rSK/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/sk-rSK/user/messages-and-channels.md b/docs/sk-rSK/user/messages-and-channels.md index 9639578354..a575edb686 100644 --- a/docs/sk-rSK/user/messages-and-channels.md +++ b/docs/sk-rSK/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/sk-rSK/user/mqtt.md b/docs/sk-rSK/user/mqtt.md index 9f850eab9f..4d2437324c 100644 --- a/docs/sk-rSK/user/mqtt.md +++ b/docs/sk-rSK/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/sk-rSK/user/node-metrics.md b/docs/sk-rSK/user/node-metrics.md index e9bf3ba9e7..64af5c3626 100644 --- a/docs/sk-rSK/user/node-metrics.md +++ b/docs/sk-rSK/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/sk-rSK/user/nodes.md b/docs/sk-rSK/user/nodes.md index 9508b244a7..9cba2e79e2 100644 --- a/docs/sk-rSK/user/nodes.md +++ b/docs/sk-rSK/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Senzor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Sledovač | TAK position reporting only | -| Straty a nálezy | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Straty a nálezy | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Popis | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Popis | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Popis | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Posledný príjem | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Vzdialenosť | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/sk-rSK/user/notifications.md b/docs/sk-rSK/user/notifications.md new file mode 100644 index 0000000000..1b4d7651a2 --- /dev/null +++ b/docs/sk-rSK/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ---------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Správy | Direct message notifications | A message sent directly to you | The conversation | +| Správy | Broadcast message notifications | A message on one of your channels | The channel | +| Správy | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Správy | Notifikácie upozornení | A critical alert from a node | The conversation | +| Mesh | Notifikácie nových uzlov | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Upozornenia o slabej batérii (obľúbene uzle) | A favorite node's battery running low | The node's details | +| Zariadenie | Notifikácie zo služby | The connection to your node while the app runs in the background | The app | +| Zariadenie | Upozornenia o slabej batérii | Your node's battery running low | The node's details | +| Zariadenie | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Zariadenie | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/sk-rSK/user/onboarding.md b/docs/sk-rSK/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/sk-rSK/user/onboarding.md +++ b/docs/sk-rSK/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/sk-rSK/user/settings-module-admin.md b/docs/sk-rSK/user/settings-module-admin.md index d809b6c7aa..8bbf181592 100644 --- a/docs/sk-rSK/user/settings-module-admin.md +++ b/docs/sk-rSK/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Konfigurácia modulu Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Popis | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Adresa | MQTT broker address | -| Používateľské meno | Authentication username | -| Heslo | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Popis | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Adresa | MQTT broker address | +| Používateľské meno | Authentication username | +| Heslo | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Popis | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Popis | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Časový limit | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Popis | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Zvonenie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Popis | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Zvonenie | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Popis | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Popis | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Popis | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Popis | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Popis | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Popis | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Popis | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Vysielať cez sieť LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Popis | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Aktualizačný interval | How often to broadcast neighbor list | +| Vysielať cez sieť LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Prúd | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Popis | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Popis | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Popis | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Popis | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Aktualizačný interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ----------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Reštartovať | Restarts the radio | -| Vypnúť | Powers the radio down | -| Obnova do výrobných nastavení | Returns every setting to its factory default | -| Reset databázy uzlov | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ----------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Reštartovať | Restarts the node | +| Vypnúť | Powers the node down | +| Obnova do výrobných nastavení | Returns every setting to its factory default | +| Reset databázy uzlov | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Zálohovanie a Obnovenie -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O aplikácii @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/sk-rSK/user/settings-radio-user.md b/docs/sk-rSK/user/settings-radio-user.md index 142fcbeece..3778a26dd2 100644 --- a/docs/sk-rSK/user/settings-radio-user.md +++ b/docs/sk-rSK/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - nastavenia - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Popis | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Dlhé Meno | Your display name (up to 39 characters) | -| Krátke Meno | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Popis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dlhé Meno | Your display name (up to 39 characters) | +| Krátke Meno | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Popis | Predvolené | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Šírka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Popis | Predvolené | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Šírka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Konfigurácia Displeju -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Popis | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Otoč Obrazovku | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Popis | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Otoč Obrazovku | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Popis | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcastový Interval | How often the position is shared with the mesh | | Inteligentná poloha | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Popis | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Konfigurácia siete -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Popis | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Heslo | Network password | | Ethernet zapnutý | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Konfigurácia Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Popis | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Popis | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Verejný kľúč | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Súkromný kľúč | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Súkromný kľúč | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/sk-rSK/user/signal-meter.md b/docs/sk-rSK/user/signal-meter.md index 1f2e39e987..ebd645378d 100644 --- a/docs/sk-rSK/user/signal-meter.md +++ b/docs/sk-rSK/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/sk-rSK/user/tak.md b/docs/sk-rSK/user/tak.md index c7196bbd5f..73e9252033 100644 --- a/docs/sk-rSK/user/tak.md +++ b/docs/sk-rSK/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/sk-rSK/user/telemetry-and-sensors.md b/docs/sk-rSK/user/telemetry-and-sensors.md index ce9ddf756e..dea3c9940b 100644 --- a/docs/sk-rSK/user/telemetry-and-sensors.md +++ b/docs/sk-rSK/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Senzor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Senzor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Senzor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiácia | µR/h | Card and chart | -| Hmotnosť | kg or lb | Card only — load cells, such as a beehive scale | -| Vzdialenosť | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiácia | µR/h | Card and chart | +| Hmotnosť | kg or lb | Card only — load cells, such as a beehive scale | +| Vzdialenosť | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/sk-rSK/user/translate.md b/docs/sk-rSK/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/sk-rSK/user/translate.md +++ b/docs/sk-rSK/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/sk-rSK/user/units-and-locale.md b/docs/sk-rSK/user/units-and-locale.md index 42c2182caa..d409839507 100644 --- a/docs/sk-rSK/user/units-and-locale.md +++ b/docs/sk-rSK/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/sk-rSK/user/widget.md b/docs/sk-rSK/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/sk-rSK/user/widget.md +++ b/docs/sk-rSK/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/sl-rSI/user/app-functions.md b/docs/sl-rSI/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/sl-rSI/user/app-functions.md +++ b/docs/sl-rSI/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/sl-rSI/user/connections.md b/docs/sl-rSI/user/connections.md index 847a914bba..e61ebb9d1a 100644 --- a/docs/sl-rSI/user/connections.md +++ b/docs/sl-rSI/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/sl-rSI/user/debug-logs.md b/docs/sl-rSI/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/sl-rSI/user/debug-logs.md +++ b/docs/sl-rSI/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/sl-rSI/user/desktop.md b/docs/sl-rSI/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/sl-rSI/user/desktop.md +++ b/docs/sl-rSI/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/sl-rSI/user/discovery.md b/docs/sl-rSI/user/discovery.md index b14485db93..499866afb5 100644 --- a/docs/sl-rSI/user/discovery.md +++ b/docs/sl-rSI/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Opis | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Opis | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/sl-rSI/user/firmware.md b/docs/sl-rSI/user/firmware.md index 2daac0cc59..d442b3f14c 100644 --- a/docs/sl-rSI/user/firmware.md +++ b/docs/sl-rSI/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/sl-rSI/user/help-and-docs.md b/docs/sl-rSI/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/sl-rSI/user/help-and-docs.md +++ b/docs/sl-rSI/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/sl-rSI/user/map-and-waypoints.md b/docs/sl-rSI/user/map-and-waypoints.md index 53c70b4da9..58bb4b4cdd 100644 --- a/docs/sl-rSI/user/map-and-waypoints.md +++ b/docs/sl-rSI/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/sl-rSI/user/messages-and-channels.md b/docs/sl-rSI/user/messages-and-channels.md index aa64fb1ed3..9effadddc7 100644 --- a/docs/sl-rSI/user/messages-and-channels.md +++ b/docs/sl-rSI/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/sl-rSI/user/mqtt.md b/docs/sl-rSI/user/mqtt.md index 848209c4c0..8a6e20322d 100644 --- a/docs/sl-rSI/user/mqtt.md +++ b/docs/sl-rSI/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/sl-rSI/user/node-metrics.md b/docs/sl-rSI/user/node-metrics.md index 3bcc552e7d..8625592c72 100644 --- a/docs/sl-rSI/user/node-metrics.md +++ b/docs/sl-rSI/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/sl-rSI/user/nodes.md b/docs/sl-rSI/user/nodes.md index 1ce945f477..6c9d18b87c 100644 --- a/docs/sl-rSI/user/nodes.md +++ b/docs/sl-rSI/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Opis | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Opis | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Opis | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Nazadnje slišano | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Razdalja | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/sl-rSI/user/notifications.md b/docs/sl-rSI/user/notifications.md new file mode 100644 index 0000000000..3c3d2d55d2 --- /dev/null +++ b/docs/sl-rSI/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Messages | Direct message notifications | A message sent directly to you | The conversation | +| Messages | Broadcast message notifications | A message on one of your channels | The channel | +| Messages | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Messages | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Obvestila novih vozlišč | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Obvestila storitve | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/sl-rSI/user/onboarding.md b/docs/sl-rSI/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/sl-rSI/user/onboarding.md +++ b/docs/sl-rSI/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/sl-rSI/user/settings-module-admin.md b/docs/sl-rSI/user/settings-module-admin.md index c9793abacb..c4ab2fec40 100644 --- a/docs/sl-rSI/user/settings-module-admin.md +++ b/docs/sl-rSI/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Nastavitev modula Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Opis | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Opis | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Opis | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Opis | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Časovna omejitev | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Opis | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Opis | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Opis | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Opis | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Opis | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Opis | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Opis | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Opis | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Opis | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Opis | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Opis | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Opis | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Opis | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Opis | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| -------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Ponovni zagon | Restarts the radio | -| Ugasni | Powers the radio down | -| Povrnitev tovarniških nastavitev | Returns every setting to its factory default | -| Ponastavi NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| -------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Ponovni zagon | Restarts the node | +| Ugasni | Powers the node down | +| Povrnitev tovarniških nastavitev | Returns every setting to its factory default | +| Ponastavi NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O programu @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/sl-rSI/user/settings-radio-user.md b/docs/sl-rSI/user/settings-radio-user.md index 024444edeb..7ea9c52f4d 100644 --- a/docs/sl-rSI/user/settings-radio-user.md +++ b/docs/sl-rSI/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Opis | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Opis | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Opis | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Opis | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Opis | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Opis | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Opis | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Opis | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Opis | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Opis | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Javni ključ | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Zasebni ključ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Zasebni ključ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/sl-rSI/user/signal-meter.md b/docs/sl-rSI/user/signal-meter.md index 8929a9e1f8..698268332d 100644 --- a/docs/sl-rSI/user/signal-meter.md +++ b/docs/sl-rSI/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/sl-rSI/user/tak.md b/docs/sl-rSI/user/tak.md index 1bc2ac7f03..a3486964dc 100644 --- a/docs/sl-rSI/user/tak.md +++ b/docs/sl-rSI/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/sl-rSI/user/telemetry-and-sensors.md b/docs/sl-rSI/user/telemetry-and-sensors.md index 3ab07ae7ee..917016ea03 100644 --- a/docs/sl-rSI/user/telemetry-and-sensors.md +++ b/docs/sl-rSI/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Razdalja | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Razdalja | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/sl-rSI/user/translate.md b/docs/sl-rSI/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/sl-rSI/user/translate.md +++ b/docs/sl-rSI/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/sl-rSI/user/units-and-locale.md b/docs/sl-rSI/user/units-and-locale.md index 35089795aa..aa7e196169 100644 --- a/docs/sl-rSI/user/units-and-locale.md +++ b/docs/sl-rSI/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/sl-rSI/user/widget.md b/docs/sl-rSI/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/sl-rSI/user/widget.md +++ b/docs/sl-rSI/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/sq-rAL/user/app-functions.md b/docs/sq-rAL/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/sq-rAL/user/app-functions.md +++ b/docs/sq-rAL/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/sq-rAL/user/connections.md b/docs/sq-rAL/user/connections.md index 46dd1160a4..8658fa1375 100644 --- a/docs/sq-rAL/user/connections.md +++ b/docs/sq-rAL/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Network | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/sq-rAL/user/debug-logs.md b/docs/sq-rAL/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/sq-rAL/user/debug-logs.md +++ b/docs/sq-rAL/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/sq-rAL/user/desktop.md b/docs/sq-rAL/user/desktop.md index 2ec0d98037..04f83db0f6 100644 --- a/docs/sq-rAL/user/desktop.md +++ b/docs/sq-rAL/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/sq-rAL/user/discovery.md b/docs/sq-rAL/user/discovery.md index ea04b614ad..11d7779e90 100644 --- a/docs/sq-rAL/user/discovery.md +++ b/docs/sq-rAL/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Përshkrimi | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Përshkrimi | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/sq-rAL/user/firmware.md b/docs/sq-rAL/user/firmware.md index cfa75569a8..18744ccf04 100644 --- a/docs/sq-rAL/user/firmware.md +++ b/docs/sq-rAL/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/sq-rAL/user/help-and-docs.md b/docs/sq-rAL/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/sq-rAL/user/help-and-docs.md +++ b/docs/sq-rAL/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/sq-rAL/user/map-and-waypoints.md b/docs/sq-rAL/user/map-and-waypoints.md index fd843de196..6b6cb72be9 100644 --- a/docs/sq-rAL/user/map-and-waypoints.md +++ b/docs/sq-rAL/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/sq-rAL/user/messages-and-channels.md b/docs/sq-rAL/user/messages-and-channels.md index 3f6e44897d..a478a79273 100644 --- a/docs/sq-rAL/user/messages-and-channels.md +++ b/docs/sq-rAL/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/sq-rAL/user/mqtt.md b/docs/sq-rAL/user/mqtt.md index 0ebee20ed7..74c945b08d 100644 --- a/docs/sq-rAL/user/mqtt.md +++ b/docs/sq-rAL/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/sq-rAL/user/node-metrics.md b/docs/sq-rAL/user/node-metrics.md index 7fe5369dd3..6a57fe01c9 100644 --- a/docs/sq-rAL/user/node-metrics.md +++ b/docs/sq-rAL/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/sq-rAL/user/nodes.md b/docs/sq-rAL/user/nodes.md index 98c704d9e0..904688de51 100644 --- a/docs/sq-rAL/user/nodes.md +++ b/docs/sq-rAL/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodes -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtrimi | Përshkrimi | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtrimi | Përshkrimi | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Përshkrimi | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | I fundit që u dëgjua | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distanca | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/sq-rAL/user/notifications.md b/docs/sq-rAL/user/notifications.md new file mode 100644 index 0000000000..d3ecadae5e --- /dev/null +++ b/docs/sq-rAL/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| 訊息 | Direct message notifications | A message sent directly to you | The conversation | +| 訊息 | Broadcast message notifications | A message on one of your channels | The channel | +| 訊息 | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| 訊息 | Alert notifications | A critical alert from a node | The conversation | +| Mesh | Njoftimet për nyje të reja | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Low battery notifications (favorite nodes) | A favorite node's battery running low | The node's details | +| Device | Njoftime shërbimi | The connection to your node while the app runs in the background | The app | +| Device | Low battery notifications | Your node's battery running low | The node's details | +| Device | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Device | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/sq-rAL/user/onboarding.md b/docs/sq-rAL/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/sq-rAL/user/onboarding.md +++ b/docs/sq-rAL/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/sq-rAL/user/settings-module-admin.md b/docs/sq-rAL/user/settings-module-admin.md index 8efe87a896..81b1f8ab08 100644 --- a/docs/sq-rAL/user/settings-module-admin.md +++ b/docs/sq-rAL/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Konfigurimi i modulit Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Përshkrimi | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Address | MQTT broker address | -| Username | Authentication username | -| Password | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Përshkrimi | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Address | MQTT broker address | +| Username | Authentication username | +| Password | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Përshkrimi | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Përshkrimi | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Koha e skaduar | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Përshkrimi | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Përshkrimi | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Përshkrimi | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Përshkrimi | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Përshkrimi | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Përshkrimi | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Përshkrimi | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Përshkrimi | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Përshkrimi | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Përshkrimi | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Përshkrimi | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Friendly name | Custom name for this sensor | +| Setting | Përshkrimi | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Përshkrimi | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Përshkrimi | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| --------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Rindiz | Restarts the radio | -| Fik | Powers the radio down | -| Përditësim i fabrikës | Returns every setting to its factory default | -| Përditësimi i NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| --------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Rindiz | Restarts the node | +| Fik | Powers the node down | +| Përditësim i fabrikës | Returns every setting to its factory default | +| Përditësimi i NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Rreth @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/sq-rAL/user/settings-radio-user.md b/docs/sq-rAL/user/settings-radio-user.md index b5ceb266e3..d069722519 100644 --- a/docs/sq-rAL/user/settings-radio-user.md +++ b/docs/sq-rAL/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Përshkrimi | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Long Name | Your display name (up to 39 characters) | -| Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Përshkrimi | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Long Name | Your display name (up to 39 characters) | +| Short Name | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Përshkrimi | Default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rajon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Përshkrimi | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Rajon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ignore MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Përshkrimi | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Përshkrimi | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Përshkrimi | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Përshkrimi | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Përshkrimi | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Përshkrimi | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Përshkrimi | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/sq-rAL/user/signal-meter.md b/docs/sq-rAL/user/signal-meter.md index 4e3f3442e3..b7c72ea04f 100644 --- a/docs/sq-rAL/user/signal-meter.md +++ b/docs/sq-rAL/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/sq-rAL/user/tak.md b/docs/sq-rAL/user/tak.md index f04441be4d..313d6f2c9a 100644 --- a/docs/sq-rAL/user/tak.md +++ b/docs/sq-rAL/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/sq-rAL/user/telemetry-and-sensors.md b/docs/sq-rAL/user/telemetry-and-sensors.md index b96e38da9e..c86ef0fadd 100644 --- a/docs/sq-rAL/user/telemetry-and-sensors.md +++ b/docs/sq-rAL/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Distanca | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Distanca | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/sq-rAL/user/translate.md b/docs/sq-rAL/user/translate.md index cdc8b6d87f..8279a6f026 100644 --- a/docs/sq-rAL/user/translate.md +++ b/docs/sq-rAL/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/sq-rAL/user/units-and-locale.md b/docs/sq-rAL/user/units-and-locale.md index 35089795aa..aa7e196169 100644 --- a/docs/sq-rAL/user/units-and-locale.md +++ b/docs/sq-rAL/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/sq-rAL/user/widget.md b/docs/sq-rAL/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/sq-rAL/user/widget.md +++ b/docs/sq-rAL/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/sr-rLatn/user/app-functions.md b/docs/sr-rLatn/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/sr-rLatn/user/app-functions.md +++ b/docs/sr-rLatn/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/sr-rLatn/user/connections.md b/docs/sr-rLatn/user/connections.md index 86fd4fffbd..a1bac1bc02 100644 --- a/docs/sr-rLatn/user/connections.md +++ b/docs/sr-rLatn/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Блутут | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Мрежа | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/sr-rLatn/user/debug-logs.md b/docs/sr-rLatn/user/debug-logs.md index 9eb603b508..c1398767ec 100644 --- a/docs/sr-rLatn/user/debug-logs.md +++ b/docs/sr-rLatn/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Дебаг логови -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/sr-rLatn/user/desktop.md b/docs/sr-rLatn/user/desktop.md index 62e33b50e1..5f837d4c6c 100644 --- a/docs/sr-rLatn/user/desktop.md +++ b/docs/sr-rLatn/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/sr-rLatn/user/discovery.md b/docs/sr-rLatn/user/discovery.md index e555756417..dd5e7bd671 100644 --- a/docs/sr-rLatn/user/discovery.md +++ b/docs/sr-rLatn/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Опис | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Опис | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/sr-rLatn/user/firmware.md b/docs/sr-rLatn/user/firmware.md index 650743c84a..9b83f10527 100644 --- a/docs/sr-rLatn/user/firmware.md +++ b/docs/sr-rLatn/user/firmware.md @@ -1,6 +1,5 @@ --- title: Ажурирања фирмвера -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/sr-rLatn/user/help-and-docs.md b/docs/sr-rLatn/user/help-and-docs.md index 7848eeae23..b7ab997a41 100644 --- a/docs/sr-rLatn/user/help-and-docs.md +++ b/docs/sr-rLatn/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/sr-rLatn/user/map-and-waypoints.md b/docs/sr-rLatn/user/map-and-waypoints.md index 164468f971..167d34f063 100644 --- a/docs/sr-rLatn/user/map-and-waypoints.md +++ b/docs/sr-rLatn/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/sr-rLatn/user/messages-and-channels.md b/docs/sr-rLatn/user/messages-and-channels.md index 8aba80d167..e595152290 100644 --- a/docs/sr-rLatn/user/messages-and-channels.md +++ b/docs/sr-rLatn/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/sr-rLatn/user/mqtt.md b/docs/sr-rLatn/user/mqtt.md index 2658a070cf..f05318cfb3 100644 --- a/docs/sr-rLatn/user/mqtt.md +++ b/docs/sr-rLatn/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Онемогућено | | **TLS enabled** | Secure connection to broker | Онемогућено | | **Map reporting** | Report position to public map | Онемогућено | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Онемогућено | +| **Proxy to client enabled** | Relay MQTT through the connected app | Онемогућено | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/sr-rLatn/user/node-metrics.md b/docs/sr-rLatn/user/node-metrics.md index d8d23e5b23..fcfac86ea3 100644 --- a/docs/sr-rLatn/user/node-metrics.md +++ b/docs/sr-rLatn/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/sr-rLatn/user/nodes.md b/docs/sr-rLatn/user/nodes.md index f5dfd7cacd..6a28c908cd 100644 --- a/docs/sr-rLatn/user/nodes.md +++ b/docs/sr-rLatn/user/nodes.md @@ -1,8 +1,7 @@ --- title: Чворови -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Сензор | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | ТАК Трекер | TAK position reporting only | -| Изгубљено и нађено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Изгубљено и нађено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Иконица | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Иконица | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Опис | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Опис | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Опис | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Poslednji put viđeno | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Udaljenost | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/sr-rLatn/user/notifications.md b/docs/sr-rLatn/user/notifications.md new file mode 100644 index 0000000000..a4d5e202cf --- /dev/null +++ b/docs/sr-rLatn/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Категорија | Posted for | Tapping it opens | +| ------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Поруке | Direct message notifications | A message sent directly to you | The conversation | +| Поруке | Broadcast message notifications | A message on one of your channels | The channel | +| Поруке | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Поруке | Обавештења о упозорењима | A critical alert from a node | The conversation | +| Mesh | Обавештење о новом чвору | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Нотификације о ниском нивоу батерије (омиљени чворови) | A favorite node's battery running low | The node's details | +| Уређај | Servisna obaveštenja | The connection to your node while the app runs in the background | The app | +| Уређај | Нотификације о ниском нивоу батерије | Your node's battery running low | The node's details | +| Уређај | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Уређај | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/sr-rLatn/user/onboarding.md b/docs/sr-rLatn/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/sr-rLatn/user/onboarding.md +++ b/docs/sr-rLatn/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/sr-rLatn/user/settings-module-admin.md b/docs/sr-rLatn/user/settings-module-admin.md index 85797c1d3b..8a3f123274 100644 --- a/docs/sr-rLatn/user/settings-module-admin.md +++ b/docs/sr-rLatn/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -30,39 +29,39 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Опис | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Адреса | MQTT broker address | -| Корисничко име | Authentication username | -| Лозинка | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Опис | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Адреса | MQTT broker address | +| Корисничко име | Authentication username | +| Лозинка | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Опис | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Опис | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Интервал објављивања мапе | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Isteklo vreme | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Опис | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Мелодија звона | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Опис | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| Трајање GPIO излаза | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Мелодија звона | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Опис | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Опис | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Инерварл пошиљаоца | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Опис | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Опис | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Опис | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Опис | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Опис | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Опис | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Интервал ажурирања | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Струја | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Опис | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Пријатељски назив | Custom name for this sensor | +| Setting | Опис | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Минимално време између емитовања детекције | Minimum time between alert broadcasts | +| Интервал емитовања стања | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Пријатељски назив | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Опис | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Опис | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Интервал ажурирања | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Поново покрени | Restarts the radio | -| Искључи | Powers the radio down | -| Рестартовање на фабричка подешавања | Returns every setting to its factory default | -| Ресетовање базе чворова | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Поново покрени | Restarts the node | +| Искључи | Powers the node down | +| Рестартовање на фабричка подешавања | Returns every setting to its factory default | +| Ресетовање базе чворова | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Напредно **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### Подешавања апликације -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### O nama @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/sr-rLatn/user/settings-radio-user.md b/docs/sr-rLatn/user/settings-radio-user.md index a9efd200ca..7c463c50cc 100644 --- a/docs/sr-rLatn/user/settings-radio-user.md +++ b/docs/sr-rLatn/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - подешавања - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Опис | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Дуго име | Your display name (up to 39 characters) | -| Кратко име | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Опис | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Дуго име | Your display name (up to 39 characters) | +| Кратко име | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Режим реемитовања | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Интервал емитовања информација о чвору | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Двоструки додир као дугме | Treat a double tap as a button press | Онемогућено | -| Троструки клик за Ad Hoc пинг | Send an ad-hoc position ping on a triple click | Онемогућено | +| Троструки клик за Ad Hoc пинг | Send an ad-hoc position ping on a triple click | Омогућено | | LED срчани откуцаји | Blink the status LED periodically | Омогућено | | Временска зона | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Опис | Подразумевано | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Унапред подешено | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Измена фреквенције | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Трансмитер укључен | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Искључен | -| Игнориши MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Позитиван за MQTT | Allow your packets to be forwarded to MQTT by gateways | Искључен | -| Појачање пријемника | Extra receive gain on SX126x radios; costs a little current | Искључен | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Искључен | +| Setting | Опис | Подразумевано | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Унапред подешено | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Измена фреквенције | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Трансмитер укључен | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Искључен | +| Игнориши MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Позитиван за MQTT | Allow your packets to be forwarded to MQTT by gateways | Искључен | +| Појачање пријемника | Extra receive gain on SX126x radios; costs a little current | Искључен | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Искључен | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Подешавања приказа -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Опис | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Екран укључен за | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Увек усмеравајте на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Опис | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Екран укључен за | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Увек усмеравајте на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Подешавања позиције On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Интервал емитовања | How often the position is shared with the mesh | | Паметно позиционирање | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Опис | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Конфигурација мреже -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Опис | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Лозинка | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Блутут подешавања -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Опис | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Javni ključ | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Privatni ključ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privatni ključ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/sr-rLatn/user/signal-meter.md b/docs/sr-rLatn/user/signal-meter.md index 71b3fb2879..100fe4e911 100644 --- a/docs/sr-rLatn/user/signal-meter.md +++ b/docs/sr-rLatn/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/sr-rLatn/user/tak.md b/docs/sr-rLatn/user/tak.md index 029bdfb879..5d2be78288 100644 --- a/docs/sr-rLatn/user/tak.md +++ b/docs/sr-rLatn/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/sr-rLatn/user/telemetry-and-sensors.md b/docs/sr-rLatn/user/telemetry-and-sensors.md index da2139b9ca..7c65924eb7 100644 --- a/docs/sr-rLatn/user/telemetry-and-sensors.md +++ b/docs/sr-rLatn/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Сензор | Метрика | Белешке | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Сензор | Метрика | Белешке | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Сензор | Метрика | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Метрика | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Udaljenost | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Метрика | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Udaljenost | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Мерни подаци о снази diff --git a/docs/sr-rLatn/user/translate.md b/docs/sr-rLatn/user/translate.md index 9516addc11..c0f97a0adb 100644 --- a/docs/sr-rLatn/user/translate.md +++ b/docs/sr-rLatn/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/sr-rLatn/user/units-and-locale.md b/docs/sr-rLatn/user/units-and-locale.md index e2fcb37e7e..88568d15ef 100644 --- a/docs/sr-rLatn/user/units-and-locale.md +++ b/docs/sr-rLatn/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/sr-rLatn/user/widget.md b/docs/sr-rLatn/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/sr-rLatn/user/widget.md +++ b/docs/sr-rLatn/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/srp/user/app-functions.md b/docs/srp/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/srp/user/app-functions.md +++ b/docs/srp/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/srp/user/connections.md b/docs/srp/user/connections.md index a7c1d668e4..a841f87f93 100644 --- a/docs/srp/user/connections.md +++ b/docs/srp/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Блутут | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Мрежа | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/srp/user/debug-logs.md b/docs/srp/user/debug-logs.md index 9eb603b508..c1398767ec 100644 --- a/docs/srp/user/debug-logs.md +++ b/docs/srp/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Дебаг логови -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/srp/user/desktop.md b/docs/srp/user/desktop.md index 62e33b50e1..5f837d4c6c 100644 --- a/docs/srp/user/desktop.md +++ b/docs/srp/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/srp/user/discovery.md b/docs/srp/user/discovery.md index e555756417..dd5e7bd671 100644 --- a/docs/srp/user/discovery.md +++ b/docs/srp/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Опис | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Опис | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/srp/user/firmware.md b/docs/srp/user/firmware.md index c23e0600fd..02634cdd97 100644 --- a/docs/srp/user/firmware.md +++ b/docs/srp/user/firmware.md @@ -1,6 +1,5 @@ --- title: Ажурирања фирмвера -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/srp/user/help-and-docs.md b/docs/srp/user/help-and-docs.md index 7848eeae23..b7ab997a41 100644 --- a/docs/srp/user/help-and-docs.md +++ b/docs/srp/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/srp/user/map-and-waypoints.md b/docs/srp/user/map-and-waypoints.md index ff57857c6a..e600b6e1a6 100644 --- a/docs/srp/user/map-and-waypoints.md +++ b/docs/srp/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/srp/user/messages-and-channels.md b/docs/srp/user/messages-and-channels.md index d6eec0c3aa..6f3569f51f 100644 --- a/docs/srp/user/messages-and-channels.md +++ b/docs/srp/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/srp/user/mqtt.md b/docs/srp/user/mqtt.md index 2658a070cf..f05318cfb3 100644 --- a/docs/srp/user/mqtt.md +++ b/docs/srp/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Онемогућено | | **TLS enabled** | Secure connection to broker | Онемогућено | | **Map reporting** | Report position to public map | Онемогућено | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Онемогућено | +| **Proxy to client enabled** | Relay MQTT through the connected app | Онемогућено | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/srp/user/node-metrics.md b/docs/srp/user/node-metrics.md index 2a3ec677e7..27cee4bf56 100644 --- a/docs/srp/user/node-metrics.md +++ b/docs/srp/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/srp/user/nodes.md b/docs/srp/user/nodes.md index 4d85cafb53..59c86912fd 100644 --- a/docs/srp/user/nodes.md +++ b/docs/srp/user/nodes.md @@ -1,8 +1,7 @@ --- title: Чворови -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Сензор | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | ТАК Трекер | TAK position reporting only | -| Изгубљено и нађено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Изгубљено и нађено | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Иконица | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Иконица | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Филтер | Опис | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Филтер | Опис | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Опис | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Последње откривање | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Раздаљина | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/srp/user/notifications.md b/docs/srp/user/notifications.md new file mode 100644 index 0000000000..d6fb34c9c5 --- /dev/null +++ b/docs/srp/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Категорија | Posted for | Tapping it opens | +| ------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Поруке | Direct message notifications | A message sent directly to you | The conversation | +| Поруке | Broadcast message notifications | A message on one of your channels | The channel | +| Поруке | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Поруке | Обавештења о упозорењима | A critical alert from a node | The conversation | +| Mesh | Обавештења о новим чворовима | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Нотификације о ниском нивоу батерије (омиљени чворови) | A favorite node's battery running low | The node's details | +| Уређај | Обавештења о услугама | The connection to your node while the app runs in the background | The app | +| Уређај | Нотификације о ниском нивоу батерије | Your node's battery running low | The node's details | +| Уређај | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Уређај | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/srp/user/onboarding.md b/docs/srp/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/srp/user/onboarding.md +++ b/docs/srp/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/srp/user/settings-module-admin.md b/docs/srp/user/settings-module-admin.md index 145cadf151..9db0d8a6a6 100644 --- a/docs/srp/user/settings-module-admin.md +++ b/docs/srp/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -30,39 +29,39 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Опис | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT enabled | Toggle MQTT bridge | -| Адреса | MQTT broker address | -| Корисничко име | Authentication username | -| Лозинка | Authentication password | -| Encryption enabled | Encrypt MQTT payloads | -| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS enabled | Use secure connection | -| Root topic | Base MQTT topic path | -| Proxy to client enabled | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Map reporting | Publish position to the public map — see the Map reporting group that follows | +| Setting | Опис | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT enabled | Toggle MQTT bridge | +| Адреса | MQTT broker address | +| Корисничко име | Authentication username | +| Лозинка | Authentication password | +| Encryption enabled | Encrypt MQTT payloads | +| JSON output enabled | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS enabled | Use secure connection | +| Root topic | Base MQTT topic path | +| Proxy to client enabled | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Опис | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Опис | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Интервал објављивања мапе | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Временско ограничење | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Опис | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| External notification enabled | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Output LED (GPIO) | Pin the LED is wired to | -| Output LED active high | Whether the LED pin is active high or low | -| Output buzzer (GPIO) | Pin the buzzer is wired to | -| Output vibra (GPIO) | Pin the vibration motor is wired to | -| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Use I2S as buzzer | Send the alert through an I2S audio output instead | -| Output duration (milliseconds) | How long a single alert lasts | -| Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Мелодија звона | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Опис | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| External notification enabled | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Output LED (GPIO) | Pin the LED is wired to | +| Output LED active high | Whether the LED pin is active high or low | +| Output buzzer (GPIO) | Pin the buzzer is wired to | +| Output vibra (GPIO) | Pin the vibration motor is wired to | +| Use PWM buzzer | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Use I2S as buzzer | Send the alert through an I2S audio output instead | +| Трајање GPIO излаза | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Мелодија звона | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Опис | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Range test enabled | Activate range testing | -| Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Опис | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Range test enabled | Activate range testing | +| Инерварл пошиљаоца | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Опис | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Environment metrics module enabled | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Air quality metrics module enabled | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Power metrics module enabled | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Power metrics on-screen enabled | Also show power readings on the device's display | +| Setting | Опис | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Environment metrics module enabled | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Environment metrics on-screen enabled | Also show these readings on the device's own display | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Air quality metrics module enabled | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Power metrics module enabled | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Power metrics on-screen enabled | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Опис | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Опис | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Опис | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Neighbor Info enabled | Activate neighbor broadcasting | -| Update interval (seconds) | How often to broadcast neighbor list | -| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Опис | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Neighbor Info enabled | Activate neighbor broadcasting | +| Интервал ажурирања | How often to broadcast neighbor list | +| Transmit over LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Струја | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Опис | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detection Sensor enabled | Activate detection sensor | -| GPIO pin to monitor | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | -| Minimum broadcast (seconds) | Minimum time between alert broadcasts | -| State broadcast (seconds) | Periodic state broadcast interval | -| Send bell with alert message | Include bell character in alerts | -| Пријатељски назив | Custom name for this sensor | +| Setting | Опис | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detection Sensor enabled | Activate detection sensor | +| GPIO pin to monitor | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Use INPUT_PULLUP mode | Enable the pin's internal pull-up resistor | +| Минимално време између емитовања детекције | Minimum time between alert broadcasts | +| Интервал емитовања стања | Periodic state broadcast interval | +| Send bell with alert message | Include bell character in alerts | +| Пријатељски назив | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Опис | -| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Paxcounter enabled | Activate people counting | -| Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Опис | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Paxcounter enabled | Activate people counting | +| Интервал ажурирања | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Поновно покретање | Restarts the radio | -| Искључи | Powers the radio down | -| Рестартовање на фабричка подешавања | Returns every setting to its factory default | -| Ресетовање базе чворова | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Поновно покретање | Restarts the node | +| Искључи | Powers the node down | +| Рестартовање на фабричка подешавања | Returns every setting to its factory default | +| Ресетовање базе чворова | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Напредно **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### Подешавања апликације -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### О @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/srp/user/settings-radio-user.md b/docs/srp/user/settings-radio-user.md index 2d11a33d82..4ad32c2391 100644 --- a/docs/srp/user/settings-radio-user.md +++ b/docs/srp/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - подешавања - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Опис | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Дуго име | Your display name (up to 39 characters) | -| Кратко име | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Опис | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Дуго име | Your display name (up to 39 characters) | +| Кратко име | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Режим реемитовања | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Интервал емитовања информација о чвору | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Двоструки додир као дугме | Treat a double tap as a button press | Онемогућено | -| Троструки клик за Ad Hoc пинг | Send an ad-hoc position ping on a triple click | Онемогућено | +| Троструки клик за Ad Hoc пинг | Send an ad-hoc position ping on a triple click | Омогућено | | LED срчани откуцаји | Blink the status LED periodically | Омогућено | | Временска зона | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Опис | Подразумевано | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Унапред подешено | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Измена фреквенције | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Трансмитер укључен | Turning this off makes the node receive-only | On | -| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Искључен | -| Игнориши MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Позитиван за MQTT | Allow your packets to be forwarded to MQTT by gateways | Искључен | -| Појачање пријемника | Extra receive gain on SX126x radios; costs a little current | Искључен | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Искључен | +| Setting | Опис | Подразумевано | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Унапред подешено | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Измена фреквенције | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Трансмитер укључен | Turning this off makes the node receive-only | On | +| Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Искључен | +| Игнориши MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Позитиван за MQTT | Allow your packets to be forwarded to MQTT by gateways | Искључен | +| Појачање пријемника | Extra receive gain on SX126x radios; costs a little current | Искључен | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Искључен | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Подешавања приказа -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Опис | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Екран укључен за | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Увек усмеравајте на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Опис | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Екран укључен за | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Display mode | Screen layout/density used by the firmware | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Flip screen | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Увек усмеравајте на север | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Подешавања позиције On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Интервал емитовања | How often the position is shared with the mesh | | Паметно позиционирање | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Опис | | ------------------------------------------------ | --------------------------------------------------------------- | -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Конфигурација мреже -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Опис | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Лозинка | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Блутут подешавања -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Опис | | ----------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Јавни кључ | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Приватни кључ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Приватни кључ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/srp/user/signal-meter.md b/docs/srp/user/signal-meter.md index 6f9ee5f431..3764174dda 100644 --- a/docs/srp/user/signal-meter.md +++ b/docs/srp/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/srp/user/tak.md b/docs/srp/user/tak.md index 029bdfb879..5d2be78288 100644 --- a/docs/srp/user/tak.md +++ b/docs/srp/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/srp/user/telemetry-and-sensors.md b/docs/srp/user/telemetry-and-sensors.md index 8de1f96fd0..a38ee4dd5e 100644 --- a/docs/srp/user/telemetry-and-sensors.md +++ b/docs/srp/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Сензор | Метрика | Белешке | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Сензор | Метрика | Белешке | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Сензор | Метрика | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Метрика | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radiation | µR/h | Card and chart | -| Weight | kg or lb | Card only — load cells, such as a beehive scale | -| Раздаљина | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Метрика | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radiation | µR/h | Card and chart | +| Weight | kg or lb | Card only — load cells, such as a beehive scale | +| Раздаљина | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Мерни подаци о снази diff --git a/docs/srp/user/translate.md b/docs/srp/user/translate.md index 9516addc11..c0f97a0adb 100644 --- a/docs/srp/user/translate.md +++ b/docs/srp/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/srp/user/units-and-locale.md b/docs/srp/user/units-and-locale.md index e2fcb37e7e..88568d15ef 100644 --- a/docs/srp/user/units-and-locale.md +++ b/docs/srp/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/srp/user/widget.md b/docs/srp/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/srp/user/widget.md +++ b/docs/srp/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/sv-rSE/user/app-functions.md b/docs/sv-rSE/user/app-functions.md index 742f89ca93..2350b55875 100644 --- a/docs/sv-rSE/user/app-functions.md +++ b/docs/sv-rSE/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: Användarguide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/sv-rSE/user/connections.md b/docs/sv-rSE/user/connections.md index 4e019d82d0..74e660b73a 100644 --- a/docs/sv-rSE/user/connections.md +++ b/docs/sv-rSE/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Nätverk | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/sv-rSE/user/debug-logs.md b/docs/sv-rSE/user/debug-logs.md index 01f74fd2de..57cca67b13 100644 --- a/docs/sv-rSE/user/debug-logs.md +++ b/docs/sv-rSE/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Användarguide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/sv-rSE/user/desktop.md b/docs/sv-rSE/user/desktop.md index dc0638b7da..7e6a24138e 100644 --- a/docs/sv-rSE/user/desktop.md +++ b/docs/sv-rSE/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/sv-rSE/user/discovery.md b/docs/sv-rSE/user/discovery.md index fa6f012be3..24758097b0 100644 --- a/docs/sv-rSE/user/discovery.md +++ b/docs/sv-rSE/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Beskrivning | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Beskrivning | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Granninformation @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/sv-rSE/user/firmware.md b/docs/sv-rSE/user/firmware.md index 4abdc33e55..3ef7c863f6 100644 --- a/docs/sv-rSE/user/firmware.md +++ b/docs/sv-rSE/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/sv-rSE/user/help-and-docs.md b/docs/sv-rSE/user/help-and-docs.md index 018dbc280f..8604e8d0a5 100644 --- a/docs/sv-rSE/user/help-and-docs.md +++ b/docs/sv-rSE/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Användarguide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/sv-rSE/user/map-and-waypoints.md b/docs/sv-rSE/user/map-and-waypoints.md index 0e25ea00b0..65183eff63 100644 --- a/docs/sv-rSE/user/map-and-waypoints.md +++ b/docs/sv-rSE/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Kartlager -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/sv-rSE/user/messages-and-channels.md b/docs/sv-rSE/user/messages-and-channels.md index a694325e21..5acca043f4 100644 --- a/docs/sv-rSE/user/messages-and-channels.md +++ b/docs/sv-rSE/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/sv-rSE/user/mqtt.md b/docs/sv-rSE/user/mqtt.md index 148410d389..ebc47c3459 100644 --- a/docs/sv-rSE/user/mqtt.md +++ b/docs/sv-rSE/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/sv-rSE/user/node-metrics.md b/docs/sv-rSE/user/node-metrics.md index a6f517c441..7d7cc2b5f5 100644 --- a/docs/sv-rSE/user/node-metrics.md +++ b/docs/sv-rSE/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/sv-rSE/user/nodes.md b/docs/sv-rSE/user/nodes.md index 08e10d7134..3239e91c20 100644 --- a/docs/sv-rSE/user/nodes.md +++ b/docs/sv-rSE/user/nodes.md @@ -1,8 +1,7 @@ --- title: Noder -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Hittegods | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Hittegods | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filter | Beskrivning | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beskrivning | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Beskrivning | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Senast hörd | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Avstånd | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/sv-rSE/user/notifications.md b/docs/sv-rSE/user/notifications.md new file mode 100644 index 0000000000..a5bb444731 --- /dev/null +++ b/docs/sv-rSE/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Meddelanden | Aviseringar om direktmeddelanden | A message sent directly to you | The conversation | +| Meddelanden | Meddelandeaviseringar | A message on one of your channels | The channel | +| Meddelanden | Waypoint-aviseringar | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Meddelanden | Larmmeddelanden | A critical alert from a node | The conversation | +| Meshnätverk | Ny nod avisering | A node heard for the first time | The node's details | +| Meshnätverk | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Meshnätverk | Meddelanden om lågt batteri (favoritnoder) | A favorite node's battery running low | The node's details | +| Enhet | Tjänsteaviseringar | The connection to your node while the app runs in the background | The app | +| Enhet | Avisering vid låg batterinivå | Your node's battery running low | The node's details | +| Enhet | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Enhet | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/sv-rSE/user/onboarding.md b/docs/sv-rSE/user/onboarding.md index 5a535b79b6..c5063e21d2 100644 --- a/docs/sv-rSE/user/onboarding.md +++ b/docs/sv-rSE/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Kom igång -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/sv-rSE/user/settings-module-admin.md b/docs/sv-rSE/user/settings-module-admin.md index 739642b788..8e20412791 100644 --- a/docs/sv-rSE/user/settings-module-admin.md +++ b/docs/sv-rSE/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Modul konfiguration Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Beskrivning | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON-utdata aktiverad | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS är aktiverat | Use secure connection | | Rotämne (root topic) | Base MQTT topic path | -| Proxy till klient aktiverad | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Proxy till klient aktiverad | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Beskrivning | -| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Jag godkänner. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Beskrivning | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Jag godkänner. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Beskrivning | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- | -| Externa aviseringar aktiverad | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Utmatning LED (GPIO) | Pin the LED is wired to | -| Utmatnings-LED aktiv hög | Whether the LED pin is active high or low | -| Utmatning summer (GPIO) | Pin the buzzer is wired to | -| Utmatning vibration (GPIO) | Pin the vibration motor is wired to | -| Använd PWM-summern | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Använd I2S som summer | Send the alert through an I2S audio output instead | -| Utmatningstid (millisekunder) | How long a single alert lasts | -| Sluta tjata efter (sekunder) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Ringsignal | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Beskrivning | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Externa aviseringar aktiverad | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Utmatning LED (GPIO) | Pin the LED is wired to | +| Utmatnings-LED aktiv hög | Whether the LED pin is active high or low | +| Utmatning summer (GPIO) | Pin the buzzer is wired to | +| Utmatning vibration (GPIO) | Pin the vibration motor is wired to | +| Använd PWM-summern | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Använd I2S som summer | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Time-out för påminnelse | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Ringsignal | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Beskrivning | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Räckvidstest aktiverat | Activate range testing | -| Avsändarens meddelandeintervall (sekunder) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Spara .CSV i enheten (endast ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Beskrivning | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Räckvidstest aktiverat | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Spara .CSV i enheten (endast ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Beskrivning | -| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Skicka enhetstelemetri | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Uppdateringsintervall för enhetsdata | How often to report battery, uptime and channel utilization | -| Mätmodul för miljö aktiverad | Report the attached environment sensors | -| Uppdateringsintervall för miljödata | How often to report them | -| Visning av miljödata på skärm är aktiverad | Also show these readings on the device's own display | -| Miljömätvärden använder Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Luftkvalitetsmätarmodul aktiverad | Report particulate and CO₂ sensor data | -| Uppdateringsintervall för luftkvalitet | How often to report them | -| Strömmätarmodul aktiverad | Report the per-channel voltage and current readings | -| Uppdateringsintervall för strömmätare | How often to report them | -| Visning av strömvärden på skärm är aktiverad | Also show power readings on the device's display | +| Setting | Beskrivning | +| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Skicka enhetstelemetri | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Uppdateringsintervall för enhetsdata | How often to report battery, uptime and channel utilization | +| Mätmodul för miljö aktiverad | Report the attached environment sensors | +| Uppdateringsintervall för miljödata | How often to report them | +| Visning av miljödata på skärm är aktiverad | Also show these readings on the device's own display | +| Miljömätvärden använder Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Luftkvalitetsmätarmodul aktiverad | Report particulate and CO₂ sensor data | +| Uppdateringsintervall för luftkvalitet | How often to report them | +| Strömmätarmodul aktiverad | Report the per-channel voltage and current readings | +| Uppdateringsintervall för strömmätare | How often to report them | +| Visning av strömvärden på skärm är aktiverad | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Beskrivning | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Beskrivning | | -------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Tillgängliga pin | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Beskrivning | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Grannskapsinformation aktiverat | Activate neighbor broadcasting | -| Uppdateringsintervall (sekunder) | How often to broadcast neighbor list | -| Skicka över LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Beskrivning | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Grannskapsinformation aktiverat | Activate neighbor broadcasting | +| Intervall för hämtning av GPS-position | How often to broadcast neighbor list | +| Skicka över LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Ström | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Beskrivning | -| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Detektionssensor aktiverad | Activate detection sensor | -| GPIO-pin att övervaka | GPIO pin connected to sensor | -| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Använd INPUT_PULLUP-läge | Enable the pin's internal pull-up resistor | -| Minsta sändningsintervall (sekunder) | Minimum time between alert broadcasts | -| Statussändningsintervall (sekunder) | Periodic state broadcast interval | -| Skicka ljudavisering med larmmeddelande | Include bell character in alerts | -| Visningsnamn | Custom name for this sensor | +| Setting | Beskrivning | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Detektionssensor aktiverad | Activate detection sensor | +| GPIO-pin att övervaka | GPIO pin connected to sensor | +| Detection trigger type | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Använd INPUT_PULLUP-läge | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Skicka ljudavisering med larmmeddelande | Include bell character in alerts | +| Visningsnamn | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Beskrivning | -| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| PAX-räknare aktiverad | Activate people counting | -| Uppdateringsintervall (sekunder) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Beskrivning | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| PAX-räknare aktiverad | Activate people counting | +| Intervall för hämtning av GPS-position | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------ | -| Ställ in tid | Sends your phone's clock to the radio | -| Starta om | Restarts the radio | -| Stäng av | Powers the radio down | -| Återställ till standardinställningar | Returns every setting to its factory default | -| Nollställ NodeDB | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------------------------------------ | ---------------------------------------------------------------------------------------------- | +| Ställ in tid | Sends your phone's clock to the node | +| Starta om | Restarts the node | +| Stäng av | Powers the node down | +| Återställ till standardinställningar | Returns every setting to its factory default | +| Nollställ NodeDB | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Säkerhetskopiering och återställning -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advancerat **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Rensa noddatabas -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Om @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/sv-rSE/user/settings-radio-user.md b/docs/sv-rSE/user/settings-radio-user.md index f1b0cef74e..e8b8c3ee37 100644 --- a/docs/sv-rSE/user/settings-radio-user.md +++ b/docs/sv-rSE/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - inställningar - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Beskrivning | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Långt namn | Your display name (up to 39 characters) | -| Kort namn | 4-character abbreviated name | -| Statusmeddelande | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Meddelanden läses ej | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensierad radioamatör (ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Beskrivning | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Långt namn | Your display name (up to 39 characters) | +| Kort namn | 4-character abbreviated name | +| Statusmeddelande | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Meddelanden läses ej | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Återutsändningsläge | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Sändningsintervall för nod-info | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Dubbeltryck som knapptryck | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Aktiverad | | LED pulsering | Blink the status LED periodically | Aktiverad | | Tidszon | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Beskrivning | Förvald | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Förval | Speed/range tradeoff | LongFast | -| Antal hopp | Maximum retransmit hops | 3 | -| Sändningseffekt | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Åsidosätt | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Använd förinställning | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spridningsfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Kodningshastighet | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandbredd | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frekvens-slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Sändning aktiverad | Turning this off makes the node receive-only | On | -| Ersätt gräns för driftsperiod | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Av | -| Ignorera MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok till MQTT | Allow your packets to be forwarded to MQTT by gateways | Av | -| RX förstärkt gain | Extra receive gain on SX126x radios; costs a little current | Av | -| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Av | +| Setting | Beskrivning | Förvald | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Förval | Speed/range tradeoff | LongFast | +| Antal hopp | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Åsidosätt | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Använd förinställning | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spridningsfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Kodningshastighet | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bandbredd | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frekvens-slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Sändning aktiverad | Turning this off makes the node receive-only | On | +| Ersätt gräns för driftsperiod | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Av | +| Ignorera MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok till MQTT | Allow your packets to be forwarded to MQTT by gateways | Av | +| RX förstärkt gain | Extra receive gain on SX126x radios; costs a little current | Av | +| PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Av | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Beskrivning | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Håll skärmen tänd | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Visningsläge | Screen layout/density used by the firmware | -| Visa enheter | Metric or Imperial on the radio's screen | -| Använd 12-timmarsformat | Show the radio's clock as 12-hour rather than 24-hour | -| Fetstil för rubriktext | Draw the screen's heading text in bold | -| Vänd skärmen | Rotate the display 180° for an inverted mounting | -| OLED-typ | Auto, SSD1306, SH1106, SH1107 | -| Vakna vid tryck eller rörelse | Light the screen when the radio is tapped or moved | -| Kompassriktning | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Peka alltid mot norr | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Beskrivning | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Håll skärmen tänd | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Visningsläge | Screen layout/density used by the firmware | +| Visa enheter | Metric or Imperial on the node's screen | +| Använd 12-timmarsformat | Show the node's clock as 12-hour rather than 24-hour | +| Fetstil för rubriktext | Draw the screen's heading text in bold | +| Vänd skärmen | Rotate the display 180° for an inverted mounting | +| OLED-typ | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Vakna vid tryck eller rörelse | Light the screen when the node is tapped or moved | +| Kompassriktning | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Peka alltid mot norr | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beskrivning | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS-läge (fysisk maskinvara) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Intervall för hämtning av GPS-position | How often the radio asks its GPS for a fix | +| Intervall för hämtning av GPS-position | How often the node asks its GPS for a fix | | Sändningsintervall | How often the position is shared with the mesh | | Smart position | Broadcast based on movement rather than purely on the clock | | Smart intervall | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Beskrivning | | -------------------------------------------------- | --------------------------------------------------------------- | -| Aktivera strömsparläge | Let the radio sleep aggressively between activity | +| Aktivera strömsparläge | Let the node sleep aggressively between activity | | Stäng av vid strömförlust | Power the device down after external power disappears | | Tid för djup strömsparläge | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Vänta in Bluetooth (sekunder) | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Beskrivning | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Lösenord | Nätverkslösenord | | Ethernet är aktiverat | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth-inställningar -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Beskrivning | | ---------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Beskrivning | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Publik nyckel | Your node's public key (read-only) | | Admin-nyckel | Keys permitted to administer this node remotely — up to three | -| Privat nyckel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Privat nyckel | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Förnya nyckel | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Seriell konsol | Serial console over the Stream API | -| API för debugloggen igång | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Hanterat läge | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API för debugloggen igång | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Hanterat läge | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Säkerhetskopiera nycklar | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/sv-rSE/user/signal-meter.md b/docs/sv-rSE/user/signal-meter.md index 9a59ea2738..ed6e615ec2 100644 --- a/docs/sv-rSE/user/signal-meter.md +++ b/docs/sv-rSE/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/sv-rSE/user/tak.md b/docs/sv-rSE/user/tak.md index 17ef124c6b..1aa253d05d 100644 --- a/docs/sv-rSE/user/tak.md +++ b/docs/sv-rSE/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/sv-rSE/user/telemetry-and-sensors.md b/docs/sv-rSE/user/telemetry-and-sensors.md index b2b21794d3..0453e04537 100644 --- a/docs/sv-rSE/user/telemetry-and-sensors.md +++ b/docs/sv-rSE/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Anteckningar | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Anteckningar | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Strålning | µR/h | Card and chart | -| Vikt | kg or lb | Card only — load cells, such as a beehive scale | -| Avstånd | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Strålning | µR/h | Card and chart | +| Vikt | kg or lb | Card only — load cells, such as a beehive scale | +| Avstånd | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Strömdata diff --git a/docs/sv-rSE/user/translate.md b/docs/sv-rSE/user/translate.md index c97b681b63..480b7d3362 100644 --- a/docs/sv-rSE/user/translate.md +++ b/docs/sv-rSE/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/sv-rSE/user/units-and-locale.md b/docs/sv-rSE/user/units-and-locale.md index 8cb121700d..ffa5c06f22 100644 --- a/docs/sv-rSE/user/units-and-locale.md +++ b/docs/sv-rSE/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/sv-rSE/user/widget.md b/docs/sv-rSE/user/widget.md index 7575a65233..250d003e55 100644 --- a/docs/sv-rSE/user/widget.md +++ b/docs/sv-rSE/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Användarguide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/tr-rTR/user/app-functions.md b/docs/tr-rTR/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/tr-rTR/user/app-functions.md +++ b/docs/tr-rTR/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/tr-rTR/user/connections.md b/docs/tr-rTR/user/connections.md index 61eea3b37c..9c09a46c15 100644 --- a/docs/tr-rTR/user/connections.md +++ b/docs/tr-rTR/user/connections.md @@ -1,8 +1,7 @@ --- title: Bağlantılar -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Ağ | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/tr-rTR/user/debug-logs.md b/docs/tr-rTR/user/debug-logs.md index be8f4bb30f..57cca67b13 100644 --- a/docs/tr-rTR/user/debug-logs.md +++ b/docs/tr-rTR/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/tr-rTR/user/desktop.md b/docs/tr-rTR/user/desktop.md index b1e640369b..eaa6b9a113 100644 --- a/docs/tr-rTR/user/desktop.md +++ b/docs/tr-rTR/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/tr-rTR/user/discovery.md b/docs/tr-rTR/user/discovery.md index f57056f030..a61b809209 100644 --- a/docs/tr-rTR/user/discovery.md +++ b/docs/tr-rTR/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Açıklaması | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Açıklaması | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Komşu Bilgisi @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/tr-rTR/user/firmware.md b/docs/tr-rTR/user/firmware.md index e5f84972f5..ae20ec40ea 100644 --- a/docs/tr-rTR/user/firmware.md +++ b/docs/tr-rTR/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/tr-rTR/user/help-and-docs.md b/docs/tr-rTR/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/tr-rTR/user/help-and-docs.md +++ b/docs/tr-rTR/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/tr-rTR/user/map-and-waypoints.md b/docs/tr-rTR/user/map-and-waypoints.md index 5897d56932..ec13778968 100644 --- a/docs/tr-rTR/user/map-and-waypoints.md +++ b/docs/tr-rTR/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Map Layers -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/tr-rTR/user/messages-and-channels.md b/docs/tr-rTR/user/messages-and-channels.md index cfb52a7b56..25a76a606c 100644 --- a/docs/tr-rTR/user/messages-and-channels.md +++ b/docs/tr-rTR/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/tr-rTR/user/mqtt.md b/docs/tr-rTR/user/mqtt.md index eb3ec5e3fb..71cc0acb5f 100644 --- a/docs/tr-rTR/user/mqtt.md +++ b/docs/tr-rTR/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/tr-rTR/user/node-metrics.md b/docs/tr-rTR/user/node-metrics.md index 59a3b4e802..7ffb3ed980 100644 --- a/docs/tr-rTR/user/node-metrics.md +++ b/docs/tr-rTR/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/tr-rTR/user/nodes.md b/docs/tr-rTR/user/nodes.md index e04a38dc18..8420e573dd 100644 --- a/docs/tr-rTR/user/nodes.md +++ b/docs/tr-rTR/user/nodes.md @@ -1,8 +1,7 @@ --- title: Nodelar -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Filtre | Açıklaması | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Açıklaması | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Açıklaması | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Son duyulma | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Mesafe | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/tr-rTR/user/notifications.md b/docs/tr-rTR/user/notifications.md new file mode 100644 index 0000000000..797c3504b1 --- /dev/null +++ b/docs/tr-rTR/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| Mesajlar | Direct message notifications | A message sent directly to you | The conversation | +| Mesajlar | Broadcast message notifications | A message on one of your channels | The channel | +| Mesajlar | Waypoint notifications | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Mesajlar | Uyarı bildirimleri | A critical alert from a node | The conversation | +| Amatör Telsiz | Yeni düğüm bildirimleri | A node heard for the first time | The node's details | +| Amatör Telsiz | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Amatör Telsiz | Düşük pil bildirimleri (favori düğümler) | A favorite node's battery running low | The node's details | +| Cihaz | Servis bildirimleri | The connection to your node while the app runs in the background | The app | +| Cihaz | Düşük pil bildirimleri | Your node's battery running low | The node's details | +| Cihaz | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Cihaz | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/tr-rTR/user/onboarding.md b/docs/tr-rTR/user/onboarding.md index 405c1e6827..c9785627be 100644 --- a/docs/tr-rTR/user/onboarding.md +++ b/docs/tr-rTR/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/tr-rTR/user/settings-module-admin.md b/docs/tr-rTR/user/settings-module-admin.md index 0039b16442..32025bceb9 100644 --- a/docs/tr-rTR/user/settings-module-admin.md +++ b/docs/tr-rTR/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +25,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Modül ayarları Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Açıklaması | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -46,23 +45,23 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | JSON çıktısı etkin | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | | TLS etkin | Use secure connection | | Ana konu | Base MQTT topic path | -| Vekilden istemciye etkin | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | +| Vekilden istemciye etkin | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | | Harita raporlama | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Açıklaması | -| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Katılıyorum. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Harita raporlama aralığı (saniye) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Açıklaması | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Katılıyorum. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Zaman Aşımı | How long to wait before considering an incoming message complete | | Konsol seri portunu geçersiz kıl | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Açıklaması | -| -------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Harici bildirim etkin | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Çıktı LED (GPIO) | Pin the LED is wired to | -| Çıktı LED aktif yüksek | Whether the LED pin is active high or low | -| Çıktı zırnı (GPIO) | Pin the buzzer is wired to | -| Çıktı titreşim (GPIO) | Pin the vibration motor is wired to | -| PWM zırnı kullan | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| I2S'yi zırnı olarak kullan | Send the alert through an I2S audio output instead | -| Çıktı süresi (milisaniye) | How long a single alert lasts | -| Nag zaman aşımı (saniye) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Zil tipi | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Açıklaması | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Harici bildirim etkin | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Çıktı LED (GPIO) | Pin the LED is wired to | +| Çıktı LED aktif yüksek | Whether the LED pin is active high or low | +| Çıktı zırnı (GPIO) | Pin the buzzer is wired to | +| Çıktı titreşim (GPIO) | Pin the vibration motor is wired to | +| PWM zırnı kullan | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| I2S'yi zırnı olarak kullan | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Zil tipi | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Açıklaması | -| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Menzi testi etkin | Activate range testing | -| Gönderen mesaj aralığı (saniye) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| .CSV'yi depolamada kaydet (sadece ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Açıklaması | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Menzi testi etkin | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| .CSV'yi depolamada kaydet (sadece ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Açıklaması | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Device metrics update interval | How often to report battery, uptime and channel utilization | -| Çevre metrikleri modülü etkin | Report the attached environment sensors | -| Environment metrics update interval | How often to report them | -| Çevre metrikleri ekran üzerinde etkin | Also show these readings on the device's own display | -| Çevre metrikleri Fahrenheit kullan | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Hava kalitesi metrikleri modülü etkin | Report particulate and CO₂ sensor data | -| Air quality metrics update interval | How often to report them | -| Güç metrikleri modülü etkin | Report the per-channel voltage and current readings | -| Power metrics update interval | How often to report them | -| Güç metrikleri ekran üzerinde etkin | Also show power readings on the device's display | +| Setting | Açıklaması | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Send Device Telemetry | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Device metrics update interval | How often to report battery, uptime and channel utilization | +| Çevre metrikleri modülü etkin | Report the attached environment sensors | +| Environment metrics update interval | How often to report them | +| Çevre metrikleri ekran üzerinde etkin | Also show these readings on the device's own display | +| Çevre metrikleri Fahrenheit kullan | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Hava kalitesi metrikleri modülü etkin | Report particulate and CO₂ sensor data | +| Air quality metrics update interval | How often to report them | +| Güç metrikleri modülü etkin | Report the per-channel voltage and current readings | +| Power metrics update interval | How often to report them | +| Güç metrikleri ekran üzerinde etkin | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Açıklaması | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Yukarı/Aşağı/Seçme girişi etkinleştirildi | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Açıklaması | | ------------------------------------ | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Tanımlanmamış pin erişimine izin ver | Allow access to any GPIO pin (security risk) | | Mevcut pinler | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Açıklaması | -| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Komşu Bilgisi etkin | Activate neighbor broadcasting | -| Güncelleme aralığı (saniye) | How often to broadcast neighbor list | -| LoRa üzerinden ilet | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Açıklaması | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Komşu Bilgisi etkin | Activate neighbor broadcasting | +| Update Interval | How often to broadcast neighbor list | +| LoRa üzerinden ilet | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +215,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Akım | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -226,39 +225,39 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | İzlenecek GPIO pini | GPIO pin connected to sensor | | Algılama tetikleme türü | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | | INPUT_PULLUP modu kullan | Enable the pin's internal pull-up resistor | -| Minimum yayın (saniye) | Minimum time between alert broadcasts | -| Durum yayını (saniye) | Periodic state broadcast interval | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | | Uyarı mesajı ile zil gönder | Include bell character in alerts | | Arkadaşça isim | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Açıklaması | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Pax sayacı etkin | Activate people counting | -| Güncelleme aralığı (saniye) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Açıklaması | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Pax sayacı etkin | Activate people counting | +| Update Interval | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ---------------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| Yeniden başlat | Restarts the radio | -| Kapat | Powers the radio down | -| Fabrika ayarları | Returns every setting to its factory default | -| NodeDB sıfırla | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| Yeniden başlat | Restarts the node | +| Kapat | Powers the node down | +| Fabrika ayarları | Returns every setting to its factory default | +| NodeDB sıfırla | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Yedekleme & Geri yükleme -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Gelişmiş **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Hakkında @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/tr-rTR/user/settings-radio-user.md b/docs/tr-rTR/user/settings-radio-user.md index 6a9edee59f..e29d91657e 100644 --- a/docs/tr-rTR/user/settings-radio-user.md +++ b/docs/tr-rTR/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ayarlar - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Açıklaması | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Uzun Ad | Your display name (up to 39 characters) | -| Kısa Ad | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Mesaj gönderilemez | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Açıklaması | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Uzun Ad | Your display name (up to 39 characters) | +| Kısa Ad | 4-character abbreviated name | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Mesaj gönderilemez | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Bilgisi Yayın Aralığı | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Açık | | LED Heartbeat | Blink the status LED periodically | Açık | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Açıklaması | Varsayılan | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Bölge | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Presets | Speed/range tradeoff | LongFast | -| Number of Hops | Maximum retransmit hops | 3 | -| Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bant genişliği | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Transmit Enabled | Turning this off makes the node receive-only | On | -| Görev Döngüsünü Geçersiz Kıl | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| MQTT'yi Yoksay | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | -| PA fanı devre dışı | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Açıklaması | Varsayılan | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Bölge | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Presets | Speed/range tradeoff | LongFast | +| Number of Hops | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Bant genişliği | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Transmit Enabled | Turning this off makes the node receive-only | On | +| Görev Döngüsünü Geçersiz Kıl | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| MQTT'yi Yoksay | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| Ok to MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | +| PA fanı devre dışı | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Akran Ayarı -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Açıklaması | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | -| Görüntü Modu | Screen layout/density used by the firmware | -| Görüntü Birimleri | Metric or Imperial on the radio's screen | -| 12h saat formatını kullan | Show the radio's clock as 12-hour rather than 24-hour | -| Bold Heading | Draw the screen's heading text in bold | -| Ekranı Çevir | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Pusula yönü | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Açıklaması | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Screen on for | How long the display stays lit before sleeping | +| Carousel interval | How often the node cycles between screens on its own | +| Görüntü Modu | Screen layout/density used by the firmware | +| Görüntü Birimleri | Metric or Imperial on the node's screen | +| 12h saat formatını kullan | Show the node's clock as 12-hour rather than 24-hour | +| Bold Heading | Draw the screen's heading text in bold | +| Ekranı Çevir | Rotate the display 180° for an inverted mounting | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Pusula yönü | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Konum Ayarı On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Açıklaması | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Açıklaması | | --------------------------------------------- | --------------------------------------------------------------- | -| Güç tasarrufu modunu etkinleştir | Let the radio sleep aggressively between activity | +| Güç tasarrufu modunu etkinleştir | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC çarpanını geçersiz kılma oranı | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Ağ Ayarı -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Açıklaması | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Şifre | Ağ şifresi | | Ethernet etkin | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Ayarı -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Açıklaması | | --------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Açıklaması | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Genel Anahtar | Your node's public key (read-only) | | Yönetici Anahtarı | Keys permitted to administer this node remotely — up to three | -| Özel Anahtar | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Özel Anahtar | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Seri konsol | Serial console over the Stream API | -| Hata ayıklama kaydı API'si etkin | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Yönetilen Mod | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Hata ayıklama kaydı API'si etkin | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Yönetilen Mod | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Anahtarları Yedekle | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/tr-rTR/user/signal-meter.md b/docs/tr-rTR/user/signal-meter.md index 4e996f3620..f3736669eb 100644 --- a/docs/tr-rTR/user/signal-meter.md +++ b/docs/tr-rTR/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/tr-rTR/user/tak.md b/docs/tr-rTR/user/tak.md index 3cf33e7e23..7e9b46afc4 100644 --- a/docs/tr-rTR/user/tak.md +++ b/docs/tr-rTR/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/tr-rTR/user/telemetry-and-sensors.md b/docs/tr-rTR/user/telemetry-and-sensors.md index 79ed0549d7..07524be8e8 100644 --- a/docs/tr-rTR/user/telemetry-and-sensors.md +++ b/docs/tr-rTR/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Sensor | Metric | Notes | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Sensor | Metric | Notes | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Radyasyon | µR/h | Card and chart | -| Ağırlık | kg or lb | Card only — load cells, such as a beehive scale | -| Mesafe | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Radyasyon | µR/h | Card and chart | +| Ağırlık | kg or lb | Card only — load cells, such as a beehive scale | +| Mesafe | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## Power Metrics diff --git a/docs/tr-rTR/user/translate.md b/docs/tr-rTR/user/translate.md index 7ec1c138d2..6917799164 100644 --- a/docs/tr-rTR/user/translate.md +++ b/docs/tr-rTR/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/tr-rTR/user/units-and-locale.md b/docs/tr-rTR/user/units-and-locale.md index dc91fc6a93..7b3b718735 100644 --- a/docs/tr-rTR/user/units-and-locale.md +++ b/docs/tr-rTR/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/tr-rTR/user/widget.md b/docs/tr-rTR/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/tr-rTR/user/widget.md +++ b/docs/tr-rTR/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/uk-rUA/user/app-functions.md b/docs/uk-rUA/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/uk-rUA/user/app-functions.md +++ b/docs/uk-rUA/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/uk-rUA/user/connections.md b/docs/uk-rUA/user/connections.md index dc44f266db..5957b231ae 100644 --- a/docs/uk-rUA/user/connections.md +++ b/docs/uk-rUA/user/connections.md @@ -1,8 +1,7 @@ --- title: Connections -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bluetooth | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| Мережа | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/uk-rUA/user/debug-logs.md b/docs/uk-rUA/user/debug-logs.md index cea9c33487..57cca67b13 100644 --- a/docs/uk-rUA/user/debug-logs.md +++ b/docs/uk-rUA/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: Посібник користувача nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/uk-rUA/user/desktop.md b/docs/uk-rUA/user/desktop.md index 22356fb409..c5aa8e983a 100644 --- a/docs/uk-rUA/user/desktop.md +++ b/docs/uk-rUA/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/uk-rUA/user/discovery.md b/docs/uk-rUA/user/discovery.md index 02f2de3977..045cccfd43 100644 --- a/docs/uk-rUA/user/discovery.md +++ b/docs/uk-rUA/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | Опис | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | Опис | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Інформація про сусідів @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/uk-rUA/user/firmware.md b/docs/uk-rUA/user/firmware.md index 4a500961f9..34b4abf851 100644 --- a/docs/uk-rUA/user/firmware.md +++ b/docs/uk-rUA/user/firmware.md @@ -1,6 +1,5 @@ --- title: Firmware Updates -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/uk-rUA/user/help-and-docs.md b/docs/uk-rUA/user/help-and-docs.md index 3853d2693a..8604e8d0a5 100644 --- a/docs/uk-rUA/user/help-and-docs.md +++ b/docs/uk-rUA/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: Посібник користувача nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/uk-rUA/user/map-and-waypoints.md b/docs/uk-rUA/user/map-and-waypoints.md index e68b433616..1720007aab 100644 --- a/docs/uk-rUA/user/map-and-waypoints.md +++ b/docs/uk-rUA/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## Шари мапи -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/uk-rUA/user/messages-and-channels.md b/docs/uk-rUA/user/messages-and-channels.md index d10c4ac7e9..9f173627e2 100644 --- a/docs/uk-rUA/user/messages-and-channels.md +++ b/docs/uk-rUA/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/uk-rUA/user/mqtt.md b/docs/uk-rUA/user/mqtt.md index be1394305a..7c30422407 100644 --- a/docs/uk-rUA/user/mqtt.md +++ b/docs/uk-rUA/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | Disabled | | **TLS enabled** | Secure connection to broker | Disabled | | **Map reporting** | Report position to public map | Disabled | -| **Proxy to client enabled** | Relay MQTT through the connected phone | Disabled | +| **Proxy to client enabled** | Relay MQTT through the connected app | Disabled | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/uk-rUA/user/node-metrics.md b/docs/uk-rUA/user/node-metrics.md index 06ab607db1..aa7488848b 100644 --- a/docs/uk-rUA/user/node-metrics.md +++ b/docs/uk-rUA/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/uk-rUA/user/nodes.md b/docs/uk-rUA/user/nodes.md index dc1e585d2f..f618555b8a 100644 --- a/docs/uk-rUA/user/nodes.md +++ b/docs/uk-rUA/user/nodes.md @@ -1,8 +1,7 @@ --- title: Вузли -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | Датчик | Optimized for telemetry reporting | | ТАК | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Loast and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Loast and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| Фільтри | Опис | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фільтри | Опис | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Опис | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Стрибків** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | Востаннє в мережі | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Відстань | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/uk-rUA/user/notifications.md b/docs/uk-rUA/user/notifications.md new file mode 100644 index 0000000000..40fd1c62d9 --- /dev/null +++ b/docs/uk-rUA/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------- | +| Повідомлення | Сповіщення особистих повідомлень | A message sent directly to you | The conversation | +| Повідомлення | Сповіщення загального каналу | A message on one of your channels | The channel | +| Повідомлення | Сповіщення про точки маршруту | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| Повідомлення | Сповіщення про тривоги | A critical alert from a node | The conversation | +| Mesh | Сповіщення про нові вузли | A node heard for the first time | The node's details | +| Mesh | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| Mesh | Сповіщення про низький рівень заряду акумулятора (улюблені вузли) | A favorite node's battery running low | The node's details | +| Пристрій | Сервісні сповіщення | The connection to your node while the app runs in the background | The app | +| Пристрій | Сповіщення про низький рівень заряду | Your node's battery running low | The node's details | +| Пристрій | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| Пристрій | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/uk-rUA/user/onboarding.md b/docs/uk-rUA/user/onboarding.md index 819bb2fa91..f460a90577 100644 --- a/docs/uk-rUA/user/onboarding.md +++ b/docs/uk-rUA/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/uk-rUA/user/settings-module-admin.md b/docs/uk-rUA/user/settings-module-admin.md index 4f0fb279fc..ec6fa99e05 100644 --- a/docs/uk-rUA/user/settings-module-admin.md +++ b/docs/uk-rUA/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Налаштування модуля Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | Опис | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MQTT увімкнений | Toggle MQTT bridge | -| Адреса | MQTT broker address | -| Ім'я користувача | Authentication username | -| Пароль | Authentication password | -| Шифрування увімкнено | Encrypt MQTT payloads | -| Вивід JSON увімкнено | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS увімкнений | Use secure connection | -| Кореневий чат | Base MQTT topic path | -| Проксі для клієнта увімкнуто | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT проксі на цьому телефоні | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| Відображення на мапі | Publish position to the public map — see the Map reporting group that follows | +| Setting | Опис | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MQTT увімкнений | Toggle MQTT bridge | +| Адреса | MQTT broker address | +| Ім'я користувача | Authentication username | +| Пароль | Authentication password | +| Шифрування увімкнено | Encrypt MQTT payloads | +| Вивід JSON увімкнено | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS увімкнений | Use secure connection | +| Кореневий чат | Base MQTT topic path | +| Проксі для клієнта увімкнуто | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| Відображення на мапі | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | Опис | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Погоджуюся. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| Інтервал звітування на мапі (секунди) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | Опис | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Погоджуюся. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,29 +75,29 @@ Enables serial port communication for external device integrations (GPS modules, | Таймаут | How long to wait before considering an incoming message complete | | Перевизначити послідовний порт | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. -| Setting | Опис | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Зовнішні сповіщення увімкнено | Master toggle for the module | -| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | -| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | -| Вихідний LED (GPIO) | Pin the LED is wired to | -| Активний високий рівень світлодіода | Whether the LED pin is active high or low | -| Вихідний гудок (GPIO) | Pin the buzzer is wired to | -| Вихід вібросигналу (GPIO) | Pin the vibration motor is wired to | -| Використовувати зумер із ШІМ-керуванням | Drive the buzzer with PWM, which allows tones rather than a single pitch | -| Використовувати I2S як гудок | Send the alert through an I2S audio output instead | -| Тривалість виводу (мілісекунд) | How long a single alert lasts | -| Інтервал нагадувань (секунди) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | -| Мелодія | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | +| Setting | Опис | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Зовнішні сповіщення увімкнено | Master toggle for the module | +| Alert message LED / buzzer / vibra | Which outputs fire on an incoming message | +| Alert bell LED / buzzer / vibra | Which outputs fire on a received bell character | +| Вихідний LED (GPIO) | Pin the LED is wired to | +| Активний високий рівень світлодіода | Whether the LED pin is active high or low | +| Вихідний гудок (GPIO) | Pin the buzzer is wired to | +| Вихід вібросигналу (GPIO) | Pin the vibration motor is wired to | +| Використовувати зумер із ШІМ-керуванням | Drive the buzzer with PWM, which allows tones rather than a single pitch | +| Використовувати I2S як гудок | Send the alert through an I2S audio output instead | +| GPIO Output Duration | How long a single alert lasts | +| Nag Timeout | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| Мелодія | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | Опис | -| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Тест на відстань увімкнений | Activate range testing | -| Інтервал надсилання повідомлень (секунди) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Зберегти .CSV у сховищі (лише ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | Опис | +| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Тест на відстань увімкнений | Activate range testing | +| Sender Interval | Time between test transmissions, chosen from a dropdown of fixed intervals | +| Зберегти .CSV у сховищі (лише ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | Опис | -| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Надсилати телеметрію пристрою | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| Інтервал оновлення показників пристрою | How often to report battery, uptime and channel utilization | -| Модуль екологічних показників увімкнено | Report the attached environment sensors | -| Інтервал оновлення екологічних показників | How often to report them | -| Екологічні показники на екрані увімкнено | Also show these readings on the device's own display | -| Екологічні показники використовують шкалу Фаренгейта | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| Модуль показників якості повітря увімкнено | Report particulate and CO₂ sensor data | -| Інтервал оновлення показників якості повітря | How often to report them | -| Модуль показників потужності ввімкнено | Report the per-channel voltage and current readings | -| Інтервал оновлення показників потужності | How often to report them | -| Показники потужності на екрані ввімкнено | Also show power readings on the device's display | +| Setting | Опис | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Надсилати телеметрію пристрою | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| Інтервал оновлення показників пристрою | How often to report battery, uptime and channel utilization | +| Модуль екологічних показників увімкнено | Report the attached environment sensors | +| Інтервал оновлення екологічних показників | How often to report them | +| Екологічні показники на екрані увімкнено | Also show these readings on the device's own display | +| Екологічні показники використовують шкалу Фаренгейта | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Модуль показників якості повітря увімкнено | Report particulate and CO₂ sensor data | +| Інтервал оновлення показників якості повітря | How often to report them | +| Модуль показників потужності ввімкнено | Report the per-channel voltage and current readings | +| Інтервал оновлення показників потужності | How often to report them | +| Показники потужності на екрані ввімкнено | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Опис | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Увімкнути керування Вгору/Вниз/Вибір | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Опис | | -------------------------------------------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Дозволити доступ до невизначених пінів | Allow access to any GPIO pin (security risk) | | Доступні піни | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. -| Setting | Опис | -| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| Інформацію про сусідів увімкнено | Activate neighbor broadcasting | -| Інтервал оновлення (секунд) | How often to broadcast neighbor list | -| Передавати через LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | +| Setting | Опис | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Інформацію про сусідів увімкнено | Activate neighbor broadcasting | +| Інтервал опитування GPS | How often to broadcast neighbor list | +| Передавати через LoRa | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Поточний | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | Опис | -| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Датчик виявлення увімкнено | Activate detection sensor | -| GPIO контакт для моніторингу | GPIO pin connected to sensor | -| Тип тригера виявлення | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| Використовувати режим INPUT_PULLUP | Enable the pin's internal pull-up resistor | -| Мінімальний період розсилки (секунди) | Minimum time between alert broadcasts | -| Інтервал трансляції стану (секунди) | Periodic state broadcast interval | -| Надсилати дзвіночок з тривожним повідомленням | Include bell character in alerts | -| Дружня назва | Custom name for this sensor | +| Setting | Опис | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Датчик виявлення увімкнено | Activate detection sensor | +| GPIO контакт для моніторингу | GPIO pin connected to sensor | +| Тип тригера виявлення | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| Використовувати режим INPUT_PULLUP | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| Надсилати дзвіночок з тривожним повідомленням | Include bell character in alerts | +| Дружня назва | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | Опис | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Лічильник пристроїв активований | Activate people counting | -| Інтервал оновлення (секунд) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | Опис | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Лічильник пристроїв активований | Activate people counting | +| Інтервал опитування GPS | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| --------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Встановити час | Sends your phone's clock to the radio | -| Перевантажити | Restarts the radio | -| Вимкнути | Powers the radio down | -| Скинути до заводських налаштувань | Returns every setting to its factory default | -| Очищення бази вузлів | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| --------------------------------- | ---------------------------------------------------------------------------------------------- | +| Встановити час | Sends your phone's clock to the node | +| Перевантажити | Restarts the node | +| Вимкнути | Powers the node down | +| Скинути до заводських налаштувань | Returns every setting to its factory default | +| Очищення бази вузлів | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Резервна копія & Відновлення -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Розширені **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Очистити базу даних вузлів -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### Про @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/uk-rUA/user/settings-radio-user.md b/docs/uk-rUA/user/settings-radio-user.md index 9d00537043..03af8d87fe 100644 --- a/docs/uk-rUA/user/settings-radio-user.md +++ b/docs/uk-rUA/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - налаштування - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | Опис | -| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Довга назва | Your display name (up to 39 characters) | -| Коротка назва | 4-character abbreviated name | -| Статус повідомлення | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| Недоступний для повідомлень | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Ліцензований радіоаматор (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | Опис | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Довга назва | Your display name (up to 39 characters) | +| Коротка назва | 4-character abbreviated name | +| Статус повідомлення | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Недоступний для повідомлень | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | Режим ретрансляції | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Інтервал розсилки даних про вузол | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Подвійний дотик як натискання кнопки | Treat a double tap as a button press | Disabled | -| Швидка перевірка зв'язку потрійним натисканням | Send an ad-hoc position ping on a triple click | Disabled | +| Швидка перевірка зв'язку потрійним натисканням | Send an ad-hoc position ping on a triple click | Увімкнено | | Частота мигання світлодіоду | Blink the status LED periodically | Увімкнено | | Часовий пояс | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | Опис | За замовчуванням | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регіон | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| Пресети | Speed/range tradeoff | LongFast | -| К-ть стрибків | Maximum retransmit hops | 3 | -| Потужність передачі | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Перевизначити частоту | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| Використовувати пресет | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Показник розширення сигналу | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| Швидкість кодування | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Ширина смуги пропускання | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| Слот частоти | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| Передача активована | Turning this off makes the node receive-only | On | -| Ігнорувати обмеження завантаженості каналу | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| Ігнорувати MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| MQTT: Готово | Allow your packets to be forwarded to MQTT by gateways | Off | -| Підсилення передачі | Extra receive gain on SX126x radios; costs a little current | Off | -| Вентилятор вимкнений | Turn off the power-amplifier fan on hardware that has one | Off | +| Setting | Опис | За замовчуванням | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Регіон | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| Пресети | Speed/range tradeoff | LongFast | +| К-ть стрибків | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| Перевизначити частоту | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Використовувати пресет | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| Показник розширення сигналу | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| Швидкість кодування | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| Ширина смуги пропускання | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| Слот частоти | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| Передача активована | Turning this off makes the node receive-only | On | +| Ігнорувати обмеження завантаженості каналу | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| Ігнорувати MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| MQTT: Готово | Allow your packets to be forwarded to MQTT by gateways | Off | +| Підсилення передачі | Extra receive gain on SX126x radios; costs a little current | Off | +| Вентилятор вимкнений | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Налаштування дисплею -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | Опис | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Екран включено для | How long the display stays lit before sleeping | -| Інтервал гортання каруселі | How often the radio cycles between screens on its own | -| Режим екрану | Screen layout/density used by the firmware | -| Одиниці виміру | Metric or Imperial on the radio's screen | -| Використовувати 12-г формат часу | Show the radio's clock as 12-hour rather than 24-hour | -| Жирні заголовки | Draw the screen's heading text in bold | -| Перевернути екран | Rotate the display 180° for an inverted mounting | -| Тип OLED | Auto, SSD1306, SH1106, SH1107 | -| Прокидання дотиком або рухом | Light the screen when the radio is tapped or moved | -| Орієнтація компаса | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| Завжди вказувати на північ | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | Опис | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Екран включено для | How long the display stays lit before sleeping | +| Інтервал гортання каруселі | How often the node cycles between screens on its own | +| Режим екрану | Screen layout/density used by the firmware | +| Одиниці виміру | Metric or Imperial on the node's screen | +| Використовувати 12-г формат часу | Show the node's clock as 12-hour rather than 24-hour | +| Жирні заголовки | Draw the screen's heading text in bold | +| Перевернути екран | Rotate the display 180° for an inverted mounting | +| Тип OLED | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Прокидання дотиком або рухом | Light the screen when the node is tapped or moved | +| Орієнтація компаса | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| Завжди вказувати на північ | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Режим GPS (Фізичний пристрій) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| Інтервал опитування GPS | How often the radio asks its GPS for a fix | +| Інтервал опитування GPS | How often the node asks its GPS for a fix | | Інтервал трансляції | How often the position is shared with the mesh | | Інтелектуальне передавання позиції | Broadcast based on movement rather than purely on the clock | | Інтелектуальний інтервал | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Опис | | ----------------------------------------------- | --------------------------------------------------------------- | -| Увімкнути енергоощадний режим | Let the radio sleep aggressively between activity | +| Увімкнути енергоощадний режим | Let the node sleep aggressively between activity | | Вимкнути при втраті живлення | Power the device down after external power disappears | | Тривалість глибокого сну | How long the deepest sleep state lasts | -| Мінімальний час в робочому режимі | The shortest time the radio stays awake once woken | +| Мінімальний час в робочому режимі | The shortest time the node stays awake once woken | | Тривалість очікування Bluetooth | How long to wait for a phone to connect before sleeping | | Корекція множника напруги | Turn on a manual correction for battery-voltage readings | | Множник корекції напруги | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Налаштування мережі -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Опис | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Пароль | Network password | | Ethernet увімкнено | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Налаштування Bluetooth -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Опис | | -------------------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Опис | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Відкритий ключ | Your node's public key (read-only) | | Ключ адміністратора | Keys permitted to administer this node remotely — up to three | -| Приватний ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Приватний ключ | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Згенерувати закритий ключ | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Серійна консоль | Serial console over the Stream API | -| API журналу відладки увімкнено | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Керований режим | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| API журналу відладки увімкнено | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Керований режим | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Резервні ключі | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/uk-rUA/user/signal-meter.md b/docs/uk-rUA/user/signal-meter.md index b3360d71bd..1e74af703f 100644 --- a/docs/uk-rUA/user/signal-meter.md +++ b/docs/uk-rUA/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/uk-rUA/user/tak.md b/docs/uk-rUA/user/tak.md index 5f0a831f86..fc226f1f7d 100644 --- a/docs/uk-rUA/user/tak.md +++ b/docs/uk-rUA/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/uk-rUA/user/telemetry-and-sensors.md b/docs/uk-rUA/user/telemetry-and-sensors.md index 0d2d925ec8..10aa12d7bf 100644 --- a/docs/uk-rUA/user/telemetry-and-sensors.md +++ b/docs/uk-rUA/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Телеметрія і сенсори -parent: Посібник користувача nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -11,7 +10,7 @@ aliases: - power-metrics --- -# Telemetry & Sensors +# Телеметрія і сенсори Meshtastic nodes can collect and share sensor data across the mesh network. Telemetry allows nodes equipped with sensors to broadcast environmental, power, and device health information, visible on the node detail screen and logged over time. @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| Датчик | Metric | Нотатки | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| Датчик | Metric | Нотатки | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Датчик | Metric | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| Metric | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| Радіація | µR/h | Card and chart | -| Вага | kg or lb | Card only — load cells, such as a beehive scale | -| Відстань | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | В | Card and chart, up to eight raw analog channels | +| Metric | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| Радіація | µR/h | Card and chart | +| Вага | kg or lb | Card only — load cells, such as a beehive scale | +| Відстань | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | В | Card and chart, up to eight raw analog channels | ## Показники живлення diff --git a/docs/uk-rUA/user/translate.md b/docs/uk-rUA/user/translate.md index 12281d56c9..f52bf2fd67 100644 --- a/docs/uk-rUA/user/translate.md +++ b/docs/uk-rUA/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/uk-rUA/user/units-and-locale.md b/docs/uk-rUA/user/units-and-locale.md index c16a6ac567..35fdc8469b 100644 --- a/docs/uk-rUA/user/units-and-locale.md +++ b/docs/uk-rUA/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/uk-rUA/user/widget.md b/docs/uk-rUA/user/widget.md index 5f3f11418b..250d003e55 100644 --- a/docs/uk-rUA/user/widget.md +++ b/docs/uk-rUA/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: Посібник користувача nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/zh-rCN/user/app-functions.md b/docs/zh-rCN/user/app-functions.md index 65132f0452..2350b55875 100644 --- a/docs/zh-rCN/user/app-functions.md +++ b/docs/zh-rCN/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/zh-rCN/user/connections.md b/docs/zh-rCN/user/connections.md index 5e0d9cefac..a8fe8c55a7 100644 --- a/docs/zh-rCN/user/connections.md +++ b/docs/zh-rCN/user/connections.md @@ -1,8 +1,7 @@ --- title: 连接数 -parent: User Guide nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP. aliases: - bluetooth @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | What it means | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | What it means | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -If no devices are found, the app shows an empty state with instructions: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 蓝牙 | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| 网络 | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### Troubleshooting Bluetooth diff --git a/docs/zh-rCN/user/debug-logs.md b/docs/zh-rCN/user/debug-logs.md index 281fecb183..57cca67b13 100644 --- a/docs/zh-rCN/user/debug-logs.md +++ b/docs/zh-rCN/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: 使用文档 nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## Desktop -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## Related Topics diff --git a/docs/zh-rCN/user/desktop.md b/docs/zh-rCN/user/desktop.md index 8effc0f051..5e8fba7c5d 100644 --- a/docs/zh-rCN/user/desktop.md +++ b/docs/zh-rCN/user/desktop.md @@ -1,6 +1,5 @@ --- title: Desktop App -parent: User Guide nav_order: 14 last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. diff --git a/docs/zh-rCN/user/discovery.md b/docs/zh-rCN/user/discovery.md index ca2b452dbb..19309ed859 100644 --- a/docs/zh-rCN/user/discovery.md +++ b/docs/zh-rCN/user/discovery.md @@ -1,8 +1,7 @@ --- title: 本地网状网络发现 -parent: User Guide nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | 说明 | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | 说明 | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +93,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +127,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,14 +141,14 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| What to look for | What it means | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | -| Many hops (4+) | Long path — consider repositioning a node to shorten it | -| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | +| What to look for | What it means | +| ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | +| Many hops (4+) | Long path — consider repositioning a node to shorten it | +| Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | > 💡 **Tip:** Run traceroute several times over a few minutes. If the path changes, your mesh has redundant routes — a sign of a well-connected network. @@ -158,7 +157,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### 邻居信息 @@ -168,42 +167,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/zh-rCN/user/firmware.md b/docs/zh-rCN/user/firmware.md index 9b5444324c..bbccf204f8 100644 --- a/docs/zh-rCN/user/firmware.md +++ b/docs/zh-rCN/user/firmware.md @@ -1,6 +1,5 @@ --- title: 固件升级 -parent: User Guide nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/zh-rCN/user/help-and-docs.md b/docs/zh-rCN/user/help-and-docs.md index c83898cea3..8604e8d0a5 100644 --- a/docs/zh-rCN/user/help-and-docs.md +++ b/docs/zh-rCN/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/zh-rCN/user/map-and-waypoints.md b/docs/zh-rCN/user/map-and-waypoints.md index 0b1101c507..19fb404df8 100644 --- a/docs/zh-rCN/user/map-and-waypoints.md +++ b/docs/zh-rCN/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: Map & Waypoints -parent: User Guide nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## 地图图层 -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/zh-rCN/user/messages-and-channels.md b/docs/zh-rCN/user/messages-and-channels.md index 8e2fdbf528..a16f641c3f 100644 --- a/docs/zh-rCN/user/messages-and-channels.md +++ b/docs/zh-rCN/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: Messages & Channels -parent: User Guide nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. diff --git a/docs/zh-rCN/user/mqtt.md b/docs/zh-rCN/user/mqtt.md index 56d99b58b0..4987e3067c 100644 --- a/docs/zh-rCN/user/mqtt.md +++ b/docs/zh-rCN/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: User Guide nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | 禁用 | | **TLS enabled** | Secure connection to broker | 禁用 | | **Map reporting** | Report position to public map | 禁用 | -| **Proxy to client enabled** | Relay MQTT through the connected phone | 禁用 | +| **Proxy to client enabled** | Relay MQTT through the connected app | 禁用 | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,19 +62,17 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### Default Meshtastic Broker The community maintains a public broker at `mqtt.meshtastic.org`. This is intended for general use and testing. -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). > 🔒 **Privacy:** Messages on the public broker are readable by anyone subscribed. Always use channel encryption for private communications. @@ -93,7 +90,7 @@ Configure your node to point to your private broker with appropriate credentials When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. diff --git a/docs/zh-rCN/user/node-metrics.md b/docs/zh-rCN/user/node-metrics.md index 489fa5a70b..c7aa9e1f39 100644 --- a/docs/zh-rCN/user/node-metrics.md +++ b/docs/zh-rCN/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/zh-rCN/user/nodes.md b/docs/zh-rCN/user/nodes.md index 395f0007a5..74eaa5dcfe 100644 --- a/docs/zh-rCN/user/nodes.md +++ b/docs/zh-rCN/user/nodes.md @@ -1,8 +1,7 @@ --- title: 节点 -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | 传感器 | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK 追踪器 | TAK position reporting only | -| 失物招领 | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| 失物招领 | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| 图标 | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| 图标 | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| 搜索节点 | 说明 | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 搜索节点 | 说明 | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | 说明 | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | 最后听到 | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | 距离 | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | Meaning | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/zh-rCN/user/notifications.md b/docs/zh-rCN/user/notifications.md new file mode 100644 index 0000000000..342590de48 --- /dev/null +++ b/docs/zh-rCN/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: Notifications +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# Notifications + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ----- | ----------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| 消息 | 私信提醒 | A message sent directly to you | The conversation | +| 消息 | 广播消息提醒 | A message on one of your channels | The channel | +| 消息 | 路径点通知 | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| 消息 | 提醒通知 | A critical alert from a node | The conversation | +| 网状网络 | 新节点通知 | A node heard for the first time | The node's details | +| 网状网络 | Mesh 组网邀请通知 | An invitation to join a nearby mesh | 本地网状网络发现 | +| 网状网络 | 低电量通知 (收藏节点) | A favorite node's battery running low | The node's details | +| 设备 | 服务通知 | The connection to your node while the app runs in the background | The app | +| 设备 | 低电量通知 | Your node's battery running low | The node's details | +| 设备 | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| 设备 | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/zh-rCN/user/onboarding.md b/docs/zh-rCN/user/onboarding.md index 993d9ec9b9..0e2ab9ede9 100644 --- a/docs/zh-rCN/user/onboarding.md +++ b/docs/zh-rCN/user/onboarding.md @@ -1,8 +1,7 @@ --- title: Getting Started -parent: User Guide nav_order: 1 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -55,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/zh-rCN/user/settings-module-admin.md b/docs/zh-rCN/user/settings-module-admin.md index 595f1d46f5..f2a25afe5c 100644 --- a/docs/zh-rCN/user/settings-module-admin.md +++ b/docs/zh-rCN/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,43 +25,43 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## 模块配置 +## 模块设定 Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| Setting | 说明 | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 启用MQTT | Toggle MQTT bridge | -| 地址 | MQTT broker address | -| 用户名称 | Authentication username | -| 密码 | Authentication password | -| 启用加密 | Encrypt MQTT payloads | -| 启用JSON输出 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| 启用TLS | Use secure connection | -| 根主题 | Base MQTT topic path | -| 启用客户端代理 | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| 在此手机上开启 MQTT 代理 | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| 地图报告 | Publish position to the public map — see the Map reporting group that follows | +| Setting | 说明 | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 启用MQTT | Toggle MQTT bridge | +| 地址 | MQTT broker address | +| 用户名称 | Authentication username | +| 密码 | Authentication password | +| 启用加密 | Encrypt MQTT payloads | +| 启用JSON输出 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| 启用TLS | Use secure connection | +| 根主题 | Base MQTT topic path | +| 启用客户端代理 | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| 地图报告 | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| Setting | 说明 | -| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 我同意。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| 地图报告间隔 (秒) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| Setting | 说明 | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 我同意。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| 地图发布间隔 | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +75,9 @@ Enables serial port communication for external device integrations (GPS modules, | 超时 | How long to wait before considering an incoming message complete | | 覆盖控制台串口端口 | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,11 +93,11 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | 输出振动 (GPIO) | Pin the vibration motor is wired to | | 使用 PWM 蜂鸣器 | Drive the buzzer with PWM, which allows tones rather than a single pitch | | 使用 I2S 作为蜂鸣器 | Send the alert through an I2S audio output instead | -| 输出持续时间 (毫秒) | How long a single alert lasts | -| 屏幕超时(秒) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| 超时提醒 | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | 铃声 | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| Setting | 说明 | -| -------------------------------------- | ----------------------------------------------------------------------------------------- | -| 启用范围测试 | Activate range testing | -| 发件人消息间隔(秒) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| 保存 CSV 到存储 (仅ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Setting | 说明 | +| -------------------------------------- | ---------------------------------------------------------------------------------------- | +| 启用范围测试 | Activate range testing | +| 发送间隔 | Time between test transmissions, chosen from a dropdown of fixed intervals | +| 保存 CSV 到存储 (仅ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| Setting | 说明 | -| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 发送设备远程数据 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| 设备计量更新间隔 | How often to report battery, uptime and channel utilization | -| 启用环境计量模块 | Report the attached environment sensors | -| 环境计量更新间隔 | How often to report them | -| 屏幕显示环境指标 | Also show these readings on the device's own display | -| 环境测量值使用华氏度 | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| 启用空气质量计量模块 | Report particulate and CO₂ sensor data | -| 空气质量计量更新间隔 | How often to report them | -| 启用电源计量模块 | Report the per-channel voltage and current readings | -| 电量计更新间隔 | How often to report them | -| 在屏幕上启用电源指标 | Also show power readings on the device's display | +| Setting | 说明 | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 发送设备远程数据 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| 设备计量更新间隔 | How often to report battery, uptime and channel utilization | +| 启用环境计量模块 | Report the attached environment sensors | +| 环境计量更新间隔 | How often to report them | +| 屏幕显示环境指标 | Also show these readings on the device's own display | +| 环境测量值使用华氏度 | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| 启用空气质量计量模块 | Report particulate and CO₂ sensor data | +| 空气质量计量更新间隔 | How often to report them | +| 启用电源计量模块 | Report the per-channel voltage and current readings | +| 电量计更新间隔 | How often to report them | +| 在屏幕上启用电源指标 | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | 说明 | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | 启用Up/Down/select 输入 | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | 说明 | | ---------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | 允许未定义的引脚访问 | Allow access to any GPIO pin (security risk) | | 可用引脚 | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. | Setting | 说明 | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 启用邻居信息 | Activate neighbor broadcasting | -| 更新间隔(秒) | How often to broadcast neighbor list | +| GPS 轮询间隔 | How often to broadcast neighbor list | | 通过 LoRa 传输 | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | 电流 | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| Setting | 说明 | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| 启用检测传感器 | Activate detection sensor | -| 显示器的 GPIO 引脚 | GPIO pin connected to sensor | -| 检测触发器类型 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| 使用 输入上拉 模式 | Enable the pin's internal pull-up resistor | -| 最小广播时间(秒) | Minimum time between alert broadcasts | -| 状态广播(秒) | Periodic state broadcast interval | -| 发送带有警报消息的响铃声 | Include bell character in alerts | -| 友好名称 | Custom name for this sensor | +| Setting | 说明 | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| 启用检测传感器 | Activate detection sensor | +| 显示器的 GPIO 引脚 | GPIO pin connected to sensor | +| 检测触发器类型 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| 使用 输入上拉 模式 | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| State Broadcast Interval | Periodic state broadcast interval | +| 发送带有警报消息的响铃声 | Include bell character in alerts | +| 友好名称 | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| Setting | 说明 | -| -------------------- | ----------------------------------------------------------------------------------------------------------------- | -| 启用 Paxcount | Activate people counting | -| 更新间隔(秒) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| Setting | 说明 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| 启用 Paxcount | Activate people counting | +| GPS 轮询间隔 | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| Action | What it does | -| ------- | ------------------------------------------------------------------------------------------------------ | -| 设置时间 | Sends your phone's clock to the radio | -| 重启 | Restarts the radio | -| 关机 | Powers the radio down | -| 恢复出厂设置 | Returns every setting to its factory default | -| 重置节点数据库 | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| Action | What it does | +| ------- | ---------------------------------------------------------------------------------------------- | +| 设置时间 | Sends your phone's clock to the node | +| 重启 | Restarts the node | +| 关机 | Powers the node down | +| 恢复出厂设置 | Returns every setting to its factory default | +| 重置节点数据库 | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### 备份与恢复 -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### 高级 **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### 清理节点数据库 -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App 设置 -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### 关于 @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/zh-rCN/user/settings-radio-user.md b/docs/zh-rCN/user/settings-radio-user.md index 303d8c42aa..5d9edbbd34 100644 --- a/docs/zh-rCN/user/settings-radio-user.md +++ b/docs/zh-rCN/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 设置 - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| Setting | 说明 | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 长名称 | Your display name (up to 39 characters) | -| 短名称 | 4-character abbreviated name | -| 状态消息 | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| 无法发送消息 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| 业余无线电模式(Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Setting | 说明 | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 长名称 | Your display name (up to 39 characters) | +| 短名称 | 4-character abbreviated name | +| 状态消息 | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| 无法发送消息 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | 转播模式 | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | 节点信息广播间隔 | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | 双击作为按钮 | Treat a double tap as a button press | 禁用 | -| 快速按3下 向所有节点发送紧急广播 | Send an ad-hoc position ping on a triple click | 禁用 | +| 快速按3下 向所有节点发送紧急广播 | Send an ad-hoc position ping on a triple click | Enabled | | LED 心跳 | Blink the status LED periodically | Enabled | | 时区 | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| Setting | 说明 | 默认 | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 区域 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| 预设 | Speed/range tradeoff | LongFast | -| 节点数 | Maximum retransmit hops | 3 | -| 发送强度 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| 频率覆盖 | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| 使用预设 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 扩散因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| 编码率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| 带宽 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| 频率时隙 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| 启用传输 | Turning this off makes the node receive-only | On | -| 覆盖占空比 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | 关闭 | -| 忽略 MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| 使用MQTT | Allow your packets to be forwarded to MQTT by gateways | 关闭 | -| RX 增益 | Extra receive gain on SX126x radios; costs a little current | 关闭 | -| PA风扇已禁用 | Turn off the power-amplifier fan on hardware that has one | 关闭 | +| Setting | 说明 | 默认 | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| 区域 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| 预设 | Speed/range tradeoff | LongFast | +| 节点数 | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| 频率覆盖 | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| 使用预设 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| 扩散因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| 编码率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| 带宽 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| 频率时隙 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| 启用传输 | Turning this off makes the node receive-only | On | +| 覆盖占空比 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | 关闭 | +| 忽略 MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| 使用MQTT | Allow your packets to be forwarded to MQTT by gateways | 关闭 | +| RX 增益 | Extra receive gain on SX126x radios; costs a little current | 关闭 | +| PA风扇已禁用 | Turn off the power-amplifier fan on hardware that has one | 关闭 | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### 屏幕配置 -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| Setting | 说明 | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 开启屏幕 | How long the display stays lit before sleeping | -| 轮播间隔 | How often the radio cycles between screens on its own | -| 显示模式 | Screen layout/density used by the firmware | -| 显示单位 | Metric or Imperial on the radio's screen | -| 使用 12 小时制格式 | Show the radio's clock as 12-hour rather than 24-hour | -| 加粗标题 | Draw the screen's heading text in bold | -| 翻转屏幕 | Rotate the display 180° for an inverted mounting | -| OLED 类型 | Auto, SSD1306, SH1106, SH1107 | -| 点击或移动时唤醒屏幕 | Light the screen when the radio is tapped or moved | -| 罗盘方向 | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| 总是朝北 | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| Setting | 说明 | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 开启屏幕 | How long the display stays lit before sleeping | +| 轮播间隔 | How often the node cycles between screens on its own | +| 显示模式 | Screen layout/density used by the firmware | +| 显示单位 | Metric or Imperial on the node's screen | +| 使用 12 小时制格式 | Show the node's clock as 12-hour rather than 24-hour | +| 加粗标题 | Draw the screen's heading text in bold | +| 翻转屏幕 | Rotate the display 180° for an inverted mounting | +| OLED 类型 | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| 点击或移动时唤醒屏幕 | Light the screen when the node is tapped or moved | +| 罗盘方向 | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| 总是朝北 | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### 定位配置 On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | 说明 | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS 模式 (物理硬件) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS 轮询间隔 | How often the radio asks its GPS for a fix | +| GPS 轮询间隔 | How often the node asks its GPS for a fix | | 广播间隔 | How often the position is shared with the mesh | | 智能位置 | Broadcast based on movement rather than purely on the clock | | 自动时间间隔 | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | 说明 | | ------------------------------------- | --------------------------------------------------------------- | -| 启用节能模式 | Let the radio sleep aggressively between activity | +| 启用节能模式 | Let the node sleep aggressively between activity | | 断电时关机 | Power the device down after external power disappears | | 深度睡眠时间 | How long the deepest sleep state lasts | -| 最小唤醒时间 | The shortest time the radio stays awake once woken | +| 最小唤醒时间 | The shortest time the node stays awake once woken | | 等待蓝牙持续时间 | How long to wait for a phone to connect before sleeping | | ADC 倍数覆盖 | Turn on a manual correction for battery-voltage readings | | ADC乘数修正比率 | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### 网络配置 -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | 说明 | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | 密码 | 网络密码 | | 启用以太网 | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### 蓝牙配置 -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | 说明 | | ------- | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | 说明 | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 公钥 | Your node's public key (read-only) | | 管理员密钥 | Keys permitted to administer this node remotely — up to three | -| 私钥 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| 私钥 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | 重新生成私钥 | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | 串口控制 | Serial console over the Stream API | -| 启用调试日志 API | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| 管理模式 | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| 启用调试日志 API | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| 管理模式 | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | 备份密钥 | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | 恢复密钥 | Write the backed-up keys back to the node (available once a backup exists) | | 删除密钥备份 | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/zh-rCN/user/signal-meter.md b/docs/zh-rCN/user/signal-meter.md index 54874bb8ee..52d488b099 100644 --- a/docs/zh-rCN/user/signal-meter.md +++ b/docs/zh-rCN/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/zh-rCN/user/tak.md b/docs/zh-rCN/user/tak.md index 509ab35ed4..2ae8bb4cfc 100644 --- a/docs/zh-rCN/user/tak.md +++ b/docs/zh-rCN/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/zh-rCN/user/telemetry-and-sensors.md b/docs/zh-rCN/user/telemetry-and-sensors.md index 9aef77da72..a2141f9eae 100644 --- a/docs/zh-rCN/user/telemetry-and-sensors.md +++ b/docs/zh-rCN/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| 传感器 | 公制 | 注 | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| 传感器 | 公制 | 注 | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | 传感器 | 公制 | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| 公制 | Unit | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| 辐射 | µR/h | Card and chart | -| 重量 | kg or lb | Card only — load cells, such as a beehive scale | -| 距离 | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | V | Card and chart, up to eight raw analog channels | +| 公制 | Unit | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| 辐射 | µR/h | Card and chart | +| 重量 | kg or lb | Card only — load cells, such as a beehive scale | +| 距离 | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | V | Card and chart, up to eight raw analog channels | ## 电源计量 diff --git a/docs/zh-rCN/user/translate.md b/docs/zh-rCN/user/translate.md index 481c48bac2..9d2dca6cb1 100644 --- a/docs/zh-rCN/user/translate.md +++ b/docs/zh-rCN/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/zh-rCN/user/units-and-locale.md b/docs/zh-rCN/user/units-and-locale.md index 25830cedb3..eb1f465cc2 100644 --- a/docs/zh-rCN/user/units-and-locale.md +++ b/docs/zh-rCN/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. diff --git a/docs/zh-rCN/user/widget.md b/docs/zh-rCN/user/widget.md index 669c79cd55..250d003e55 100644 --- a/docs/zh-rCN/user/widget.md +++ b/docs/zh-rCN/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/docs/zh-rTW/user/app-functions.md b/docs/zh-rTW/user/app-functions.md index 48d667abf1..95eafa4626 100644 --- a/docs/zh-rTW/user/app-functions.md +++ b/docs/zh-rTW/user/app-functions.md @@ -1,6 +1,5 @@ --- title: App Functions -parent: User Guide nav_order: 19 last_updated: 2026-08-30 description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. diff --git a/docs/zh-rTW/user/connections.md b/docs/zh-rTW/user/connections.md index d6805bfe9b..c7737fe6bc 100644 --- a/docs/zh-rTW/user/connections.md +++ b/docs/zh-rTW/user/connections.md @@ -1,8 +1,7 @@ --- title: 連線 -parent: 使用者指南 nav_order: 2 -last_updated: 2026-08-30 +last_updated: 2026-09-29 description: 透過藍牙、USB 或 TCP/IP 將您的手機或電腦連接至 Meshtastic 無線電裝置。 aliases: - 藍牙 @@ -39,12 +38,12 @@ Use the transport selector — a segmented button row below the connection card The screen names anything on the app's side that is blocking a scan, with the fix attached: -| What you see | 代表意義 | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | -| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | -| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | -| No card, empty list | Nothing on this side is blocking the scan — the radio is out of range, off, or already connected elsewhere. | +| What you see | 代表意義 | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A card asking for **Nearby devices** | The permission has not been granted. **Grant permission** requests it; once Android stops prompting, the button becomes **Open settings**. | +| **Bluetooth is off** | The adapter is disabled — the card opens Bluetooth settings. | +| **Bluetooth scanning also needs location services** | Android 11 and older only: the permission is held but the system location toggle is off. | +| No card, and **No Bluetooth devices seen** under the header | Nothing on this side is blocking the scan. The radio is out of range, off, or already connected elsewhere. | The explanation lives in that card, not in the scan control: tapping **Scan for Bluetooth devices** after you have declined once asks Android again directly. @@ -63,9 +62,13 @@ When connecting, a status indicator shows the current connection state — tap * ![Connecting status](../../assets/screenshots/connections_connecting.png) -若未找到任何裝置,應用程式將顯示空白畫面並提供操作說明: +With no radio chosen yet, the connection card reads **No device selected**. A pane with nothing to list says so under its header, with a hint: -![No devices found](../../assets/screenshots/connections_empty_state.png) +| Pane | What it shows | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 藍牙 | **No Bluetooth devices seen**, and "Ensure you're within range of the device." Start a scan from the header. | +| 網路 | **No network devices seen**, and "Ensure you're connected to the same network as the device." Shown only while nothing has been discovered and **Recent Network Devices** is empty. **Add device manually…** stays below it. | +| USB | **No USB devices detected**, and "Connect a device with a USB data cable to use serial." | ### 藍牙疑難排解 diff --git a/docs/zh-rTW/user/debug-logs.md b/docs/zh-rTW/user/debug-logs.md index ced8715f39..a3e310a1f2 100644 --- a/docs/zh-rTW/user/debug-logs.md +++ b/docs/zh-rTW/user/debug-logs.md @@ -1,8 +1,7 @@ --- title: Debug Logs -parent: User Guide nav_order: 22 -last_updated: 2026-08-30 +last_updated: 2026-09-28 description: View and export the app's own debug logs from inside the app, and attach a capture to a GitHub issue to help diagnose bugs — no adb required. aliases: - debug-logs @@ -48,7 +47,7 @@ Attach that file to your GitHub issue. ## 桌面版 -The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. +The desktop app has no system logcat, so the **App logs** tab shows the app's own captured log output instead. Search, filtering, and export work the same way. Release builds capture Info, Warn, and Error lines; Verbose and Debug lines appear only in development builds. ## 相關主題 diff --git a/docs/zh-rTW/user/desktop.md b/docs/zh-rTW/user/desktop.md index 9ca528b242..3a5032f5b7 100644 --- a/docs/zh-rTW/user/desktop.md +++ b/docs/zh-rTW/user/desktop.md @@ -1,6 +1,5 @@ --- title: 桌面版應用程式 -parent: 使用者指南 nav_order: 14 last_updated: 2026-09-11 description: 在 Linux、macOS 及 Windows 上安裝並使用 Meshtastic 桌面版應用程式——涵蓋連線方式、功能對等性與鍵盤快速鍵。 @@ -53,7 +52,7 @@ Connect your radio via USB. The app detects the serial port automatically; if it Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/JuulLabs/kable) library: -1. 請確認您的系統配備藍牙介面卡。 應用程式將自動掃描附近的 Meshtastic 無線電裝置。 +1. 請確認您的系統配備藍牙介面卡。應用程式將自動掃描附近的 Meshtastic 無線電裝置。 2. Select your radio from the Connect screen. ## 功能對等性 diff --git a/docs/zh-rTW/user/discovery.md b/docs/zh-rTW/user/discovery.md index 5548163d10..57f5d3531d 100644 --- a/docs/zh-rTW/user/discovery.md +++ b/docs/zh-rTW/user/discovery.md @@ -1,8 +1,7 @@ --- title: Local Mesh Discovery -parent: 使用者指南 nav_order: 12 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,53 +19,53 @@ aliases: The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: -| Control | 描述說明 | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | -| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| Control | 描述說明 | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: -| Stage | What's happening | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | -| **Reconnecting on \** | Re-establishing the connection after the preset change. | -| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | -| **Analyzing results** | Processing the collected packets and ranking the presets. | -| **Restoring home preset** | Putting your original LoRa configuration back. | -| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| Stage | What's happening | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Preparing scan** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the node to the next preset to test. | +| **Reconnecting on \** | Re-establishing the connection after the preset change. | +| **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analyzing results** | Processing the collected packets and ranking the presets. | +| **Restoring home preset** | Putting your original LoRa configuration back. | +| **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### 解讀結果 +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,41 +93,41 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but _Set your radio's region before setting up a beacon._ Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but _Set your node's region before setting up a beacon._ Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. ### 路由追蹤 -路由追蹤可顯示訊息從您的節點到 mesh 網路上任一節點所經過的確切路徑。 這是診斷連線問題最有效的工具。 +路由追蹤可顯示訊息從您的節點到 mesh 網路上任一節點所經過的確切路徑。這是診斷連線問題最有效的工具。 #### 執行路由追蹤 1. 前往「節點」,並點選您要追蹤的節點。 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### 解讀結果 +#### Reading the results 路由追蹤結果的顯示方式如下: @@ -142,68 +141,68 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. -| 判讀重點 | 代表意義 | -| ------------------------------------------------------------------ | ------------------------------ | -| All hops show Good SNR (≥ −7 dB, green) | 路徑狀況良好 — 訊息可穩定傳送 | -| One hop shows a poor SNR (below −15 dB, orange) | 訊號薄弱 — 此中繼路段不穩定 | -| 跳躍點過多(4 個以上) | 路徑過長 — 建議調整節點位置以縮短路徑 | -| 重試時走不同路徑 | Mesh 網路正在自動調整 — 存在多條路由(這是好現象!) | +| 判讀重點 | 代表意義 | +| ----------------------------------------------------------- | ------------------------------ | +| All hops show Good SNR (green) | 路徑狀況良好 — 訊息可穩定傳送 | +| One hop shows a poor SNR (orange or red) | 訊號薄弱 — 此中繼路段不穩定 | +| 跳躍點過多(4 個以上) | 路徑過長 — 建議調整節點位置以縮短路徑 | +| 重試時走不同路徑 | Mesh 網路正在自動調整 — 存在多條路由(這是好現象!) | -> 💡 提示:請在幾分鐘內多次執行路由追蹤。 若路徑發生變化,代表您的 mesh 網路具備備援路由 — 這是網路連線良好的象徵。 +> 💡 提示:請在幾分鐘內多次執行路由追蹤。若路徑發生變化,代表您的 mesh 網路具備備援路由 — 這是網路連線良好的象徵。 #### 使用路由追蹤進行疑難排解 - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. 請確認兩個節點至少共用一個使用相同加密金鑰的頻道。 - 「路由追蹤逾時」— 路徑可能過長(超過跳躍限制),或某中繼節點發生壅塞。 Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- 非對稱路徑 — 從 A → B 的路由追蹤路徑,可能與 B → A 不同。 這屬於正常現象 — 無線電訊號的傳播並不總是對稱的。 +- 非對稱路徑 — 從 A → B 的路由追蹤路徑,可能與 B → A 不同。 This is normal — radio propagation isn't always symmetric. ### 鄰近節點資訊 -鄰近節點資訊模組可讓每個節點廣播其可直接收到訊號的節點清單(單跳躍)。 當多個節點共享各自的鄰近節點清單時,您便可拼湊出整個 mesh 網路的拓撲圖。 +鄰近節點資訊模組可讓每個節點廣播其可直接收到訊號的節點清單(單跳躍)。當多個節點共享各自的鄰近節點清單時,您便可拼湊出整個 mesh 網路的拓撲圖。 #### 啟用鄰近節點資訊 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. 啟用此模組。 -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update Interval**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. 其他已啟用鄰近節點資訊的節點也會執行相同動作。 -#### 檢視鄰近節點資料 +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - 每筆鄰近節點記錄會顯示可直接收到訊號的節點及其訊號品質。 - 結合多個節點的鄰近節點資料,以了解完整的 mesh 網路拓撲。 -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### 將節點清單作為探索工具 +### Node list as a discovery tool 善用節點清單的篩選與排序功能,即可將其作為強大的探索工具。 -#### 尋找新節點 +#### Finding new nodes - 依「最後收到訊號」排序,可將最近有活動的節點顯示於頂部。 -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### 評估連線狀況 +#### Assessing connectivity - 依「跳躍距離」排序,可區分可直接到達的節點(0 個跳躍點)與需中繼轉送的節點。 - 依「距離」排序,可找出附近的節點並確認是否可到達。 -- 使用「排除 MQTT」,可專注於透過無線電(而非網際網路橋接)可到達的節點。 +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### 基礎架構稽核 +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - 檢查其訊號品質與最後收到訊號的時間,以確認基礎架構節點運作正常。 請參閱〔節點〕(nodes) 以了解完整的篩選與排序選項說明。 -## Mesh 網路探索技巧 +## Tips for Mesh exploration - 從路由追蹤開始 — 可立即取得特定路徑的具體可行資訊。 - 在關鍵節點上啟用鄰近節點資訊 — 尤其是路由器與中繼器,以建立骨幹網路的整體概況。 diff --git a/docs/zh-rTW/user/firmware.md b/docs/zh-rTW/user/firmware.md index c9e10edf70..66ee4969dd 100644 --- a/docs/zh-rTW/user/firmware.md +++ b/docs/zh-rTW/user/firmware.md @@ -1,6 +1,5 @@ --- title: 韌體更新 -parent: 使用者指南 nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -76,6 +75,8 @@ Both a USB erase and a bootloader upgrade write two files in turn, so you are as The app reads `INFO_UF2.TXT` from the drive you select to confirm it really is the device's update drive and to identify the board before writing anything. +For a bootloader upgrade, the app also reads the installed bootloader version from `INFO_UF2.TXT` and shows it next to the latest release before writing anything. The running firmware doesn't report its bootloader, so the installed version appears only once the device has restarted into update mode, never on the firmware screen while connected. If the two match, the bootloader is left as it is and the app moves straight on to reinstalling the firmware. Otherwise choose **Upgrade bootloader** to write it, or **Skip** to reinstall the firmware without changing it. + On nRF52 the app must already know which Bluetooth stack your device uses before it starts, because it can't read the bootloader until the device has rebooted. If it can't confirm the stack, it refuses to erase and points you at the [Web Flasher](https://flasher.meshtastic.org) instead. In the Web Flasher, choosing the wrong Bluetooth stack can leave the radio recoverable only with a hardware programmer. Once the drive is readable, how an nRF52 device is erased depends on its bootloader: diff --git a/docs/zh-rTW/user/help-and-docs.md b/docs/zh-rTW/user/help-and-docs.md index 559bbb157c..b822720a01 100644 --- a/docs/zh-rTW/user/help-and-docs.md +++ b/docs/zh-rTW/user/help-and-docs.md @@ -1,6 +1,5 @@ --- title: Help & In-App Docs -parent: User Guide nav_order: 21 last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. diff --git a/docs/zh-rTW/user/map-and-waypoints.md b/docs/zh-rTW/user/map-and-waypoints.md index 8c76a667f4..a631307daa 100644 --- a/docs/zh-rTW/user/map-and-waypoints.md +++ b/docs/zh-rTW/user/map-and-waypoints.md @@ -1,8 +1,7 @@ --- title: 地圖與航點 -parent: 使用者指南 nav_order: 6 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -102,7 +101,7 @@ Since waypoints (and their geofences) are broadcast to the whole mesh, only the ## 地圖圖層 -Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format — including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `http://` or `https://` URL pointing at a KML or GeoJSON file; that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. +Tap the layers icon on the map to open **Manage Map Layers**. It imports your own overlays in `.kml`, `.kmz`, or GeoJSON format, including KMZ ground overlays (georeferenced images, such as exported topo or aerial tiles), which drape at their stated bounds. Add one by picking a file with **Add Layer**, opening a file with Meshtastic, or sharing it into the app from another app. **Add Network Layer** instead takes a name and an `https://` URL pointing at a KML or GeoJSON file (`http://` also works on Desktop, but on Android only for `localhost`); that layer then carries its own refresh button in the sheet. On **Google Play** builds the toolbar's refresh button re-fetches every visible network layer at once. Imported layers are listed with a toggle to show/hide each one and an option to remove it. Each layer — imported or built-in overlay — carries its own opacity slider while it is switched on, so an overlay can be faded back rather than only switched off. This works on the Google Play build, the F-Droid build, and **Desktop**, which shares the same layer store and file picker. @@ -125,7 +124,7 @@ Configure position behavior in **Settings → Device configuration → Position* ### 隱私注意事項 -> 🔒 隱私:位置資料將廣播至您頻道上的所有節點。 若不想分享您的位置,請在設定中停用 GPS 定位,或使用固定/假位置。 To keep sharing a position without pinpointing yourself, edit the channel in **Settings → Channels**, turn **Precise location** off, and set the slider beneath it — the channel then publishes an approximate area, shown as ± a distance, instead of an exact point. +> 🔒 隱私:位置資料將廣播至您頻道上的所有節點。若不想分享您的位置,請在設定中停用 GPS 定位,或使用固定/假位置。 To keep sharing a position without pinpointing yourself, edit the channel in **Settings → Channels**, turn **Precise location** off, and set the slider beneath it — the channel then publishes an approximate area, shown as ± a distance, instead of an exact point. ## 地圖來源 @@ -161,6 +160,8 @@ Tile Sources** at the foot of the base map picker and paste a URL template using https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` +On **Android** the template must use `https://`; plain `http://` works only for `localhost`. + Tiles are cached on disk, so panning does not re-download what you were just looking at. On **Android**, the same screen also imports a local `.mbtiles` archive for fully offline use. diff --git a/docs/zh-rTW/user/messages-and-channels.md b/docs/zh-rTW/user/messages-and-channels.md index c86f1d4f08..b6ff06048c 100644 --- a/docs/zh-rTW/user/messages-and-channels.md +++ b/docs/zh-rTW/user/messages-and-channels.md @@ -1,6 +1,5 @@ --- title: 訊息與頻道 -parent: 使用者指南 nav_order: 3 last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. @@ -17,7 +16,7 @@ Meshtastic 支援兩種通訊模式:頻道廣播與私訊。 ## 頻道 -頻道是共享的通訊群組。 所有設定相同頻道金鑰的節點均可在該頻道上讀取與傳送訊息。 +頻道是共享的通訊群組。所有設定相同頻道金鑰的節點均可在該頻道上讀取與傳送訊息。 ### 預設頻道 @@ -124,7 +123,7 @@ A status label appears under **your own** outgoing messages only (incoming messa | 錯誤 | 含義 | 處理方式 | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| 無路徑 | 無法找到通往目標節點的路徑 | 收件者可能已離線或超出 mesh 網路範圍。 請稍後再試,或靠近對方後重新傳送。 | +| 無路徑 | 無法找到通往目標節點的路徑 | 收件者可能已離線或超出 mesh 網路範圍。請稍後再試,或靠近對方後重新傳送。 | | No radio interface | 無可用的無線電介面進行傳送 | Check that your radio is connected and available. | | Failed to deliver to mesh | Retries exhausted. The same label covers three underlying causes — a relay refusing (NAK), a plain timeout, and running out of retransmits | Move closer, improve signal, or wait for conditions to improve. Tap the error for the specific cause. | | Rate limited | The mesh is throttling you for sending too fast | Wait before sending again. | @@ -140,7 +139,7 @@ A status label appears under **your own** outgoing messages only (incoming messa | Duty cycle limit | 已達地區無線電佔用時間上限 | Wait for the duty cycle window to reset. | | Invalid request | Malformed or invalid request | Retry after updating or restarting the app if this persists. | -> 💡 提示:大多數傳遞錯誤會自動解決。 若節點間歇性可到達,mesh 網路將自動重試。 For persistent **No route** errors, check that intermediate Router nodes are online. +> 💡 提示:大多數傳遞錯誤會自動解決。若節點間歇性可到達,mesh 網路將自動重試。 For persistent **No route** errors, check that intermediate Router nodes are online. ## 訊息功能 @@ -173,7 +172,7 @@ You can search the full history of any conversation directly from the chat scree ### 訊息泡泡 -訊息以對話泡泡的形式顯示 — 已傳送的訊息在右側,收到的訊息在左側。 每個泡泡顯示傳送者、時間戳記及傳遞狀態。 含有回覆的訊息,會在回覆內容上方顯示原始訊息的引用預覽。 +訊息以對話泡泡的形式顯示 — 已傳送的訊息在右側,收到的訊息在左側。每個泡泡顯示傳送者、時間戳記及傳遞狀態。含有回覆的訊息,會在回覆內容上方顯示原始訊息的引用預覽。 ### Text Formatting diff --git a/docs/zh-rTW/user/mqtt.md b/docs/zh-rTW/user/mqtt.md index a722757fc9..3148be61e1 100644 --- a/docs/zh-rTW/user/mqtt.md +++ b/docs/zh-rTW/user/mqtt.md @@ -1,8 +1,7 @@ --- title: MQTT -parent: 使用者指南 nav_order: 11 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: 將您的 mesh 網路橋接至網際網路 — MQTT 代理伺服器設定、加密層級與地圖回報。 aliases: - MQTT @@ -49,13 +48,13 @@ A gateway node with internet access (Wi-Fi or Ethernet) publishes mesh messages | **JSON output enabled** | Also publish and consume the `/2/json/` topic. Deprecated in the protobuf schema, but still the only toggle for this behavior — and the app's own proxy honors it | 已停用 | | **TLS enabled** | 與代理伺服器的安全連線 | 已停用 | | **Map reporting** | 將位置回報至公開地圖 | 已停用 | -| **Proxy to client enabled** | Relay MQTT through the connected phone | 已停用 | +| **Proxy to client enabled** | Relay MQTT through the connected app | 已停用 | ### Connection Status and Test Connection -The top of the MQTT settings screen shows the status of the relay this phone runs — +The top of the MQTT settings screen shows the status of the relay this app runs: **Connected**, **Connecting**, **Reconnecting**, **Disconnected**, or **Inactive**. It reads -**Inactive** whenever the phone is not relaying, which includes the normal case of a radio +**Inactive** whenever the app is not relaying, which includes the normal case of a radio reaching the broker over its own Wi-Fi or Ethernet. The radio's own connection to the broker is not reported here. @@ -63,21 +62,19 @@ not reported here. distinguishes the failure modes: the hostname not resolving, the TCP connection being refused, TLS failing, the attempt timing out, or the broker rejecting your credentials with a reason. -### MQTT Proxy on This Phone +### MQTT Proxy in This App -If your radio has no internet access of its own, it can use the connected phone as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's internet connection. +If your radio has no internet access of its own, it can use the app as its MQTT gateway: enable **MQTT** and **Proxy to client enabled** in the module config, and the app relays MQTT traffic between the radio and the broker over your phone's or computer's internet connection. -> ℹ️ **Note:** The proxy relay is mobile-only. On the Desktop app the MQTT settings are present, but no relay runs behind them. - -The **MQTT proxy on this phone** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately — without editing and re-saving the radio's MQTT configuration. +The **MQTT proxy in this app** toggle at the top of the MQTT settings screen shows whether this relay is running and lets you cut it off (or restart it) immediately, without editing and re-saving the radio's MQTT configuration. ### 預設 Meshtastic 代理伺服器 -社群在 mqtt.meshtastic.org 維護一個公開的代理伺服器。 此伺服器供一般使用與測試之用。 +社群在 mqtt.meshtastic.org 維護一個公開的代理伺服器。此伺服器供一般使用與測試之用。 -When this phone relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off — the app forces the switch on and grays it out. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: turn **TLS enabled** on yourself, or it connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). +When this app relays MQTT for the radio, connections to that broker always use TLS on port 8883 even if **TLS enabled** is off. That upgrade is the app's own, and the switch says so. A radio that reaches the broker over its own Wi-Fi or Ethernet forces nothing: it uses **TLS enabled** as stored, so turn it on yourself or the radio connects in the clear on port 1883. For any other broker the toggle decides in both cases (port 8883 with TLS, 1883 without). -> 🔒 隱私:公開代理伺服器上的訊息,任何訂閱者均可讀取。 私人通訊請務必啟用頻道加密。 +> 🔒 隱私:公開代理伺服器上的訊息,任何訂閱者均可讀取。私人通訊請務必啟用頻道加密。 ### 私有代理伺服器 @@ -93,7 +90,7 @@ When this phone relays MQTT for the radio, connections to that broker always use When **Map reporting** is on, your node periodically publishes a map report to the broker. The report goes out unencrypted, whatever keys your channels use, and carries your node id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset, and primary channel name. -Turning it on opens a consent card. Turn on **I agree.** and choose a **Map reporting interval (seconds)** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. +Turning it on opens a consent card. Turn on **I agree.** and choose a **Map Publish Interval** of one hour or more — the screen will not save until you do. A slider sets the position precision, and the app shows the resulting accuracy as a ± distance, so you can publish an approximate location rather than an exact one. Reports appear at [meshmap.net](https://meshmap.net) and similar community map services. @@ -125,11 +122,11 @@ MQTT carries two payload formats: 了解分層加密模型: -1. 頻道加密在 mesh 網路上進行,發生於 MQTT 傳輸之前。 若您的頻道已設定 PSK,MQTT 承載內容將已加密——代理伺服器及任何訂閱者只能看到密文。 +1. 頻道加密在 mesh 網路上進行,發生於 MQTT 傳輸之前。若您的頻道已設定 PSK,MQTT 承載內容將已加密——代理伺服器及任何訂閱者只能看到密文。 2. **Encryption enabled** (the module setting) decides which copy of the packet the gateway publishes — it is not an extra layer. Leave it on and the broker receives the packet still encrypted with your channel key. Turn it off and the gateway publishes the decrypted packet, so anyone subscribed to the topic reads your messages in the clear. Turn it off only when you own the broker and want plain payloads for a dashboard. 3. TLS 對連接至代理伺服器的 TCP 連線進行加密,防止網路層級的竊聽。 -> 🔒 **Security:** The default public channel has a well-known key. 透過 MQTT 傳送的預設頻道訊息實際上等同於未加密 — 任何人均可解碼。 私人通訊請務必使用自訂 PSK。 +> 🔒 **Security:** The default public channel has a well-known key. 透過 MQTT 傳送的預設頻道訊息實際上等同於未加密 — 任何人均可解碼。私人通訊請務必使用自訂 PSK。 ## 最佳實踐 @@ -146,12 +143,12 @@ MQTT carries two payload formats: - **Check Wi-Fi** — the gateway node must have an active internet connection (Wi-Fi or Ethernet). MQTT 無法直接透過 LoRa 無線電連結運作。 - **Verify credentials** — with incorrect credentials, most brokers fail silently — double-check for trailing spaces. - **Firewall** — port 1883 (MQTT) or 8883 (MQTT over TLS) must be reachable. Some networks allow only web traffic (ports 80 and 443). -- DNS 解析 — 若使用自訂代理伺服器主機名稱,請確認節點能夠正確解析該名稱。 請嘗試直接使用代理伺服器的 IP 位址進行連線。 +- DNS 解析 — 若使用自訂代理伺服器主機名稱,請確認節點能夠正確解析該名稱。請嘗試直接使用代理伺服器的 IP 位址進行連線。 ### 訊息未正常橋接 -- 檢查上行/下行設定 — 若僅啟用上行,訊息只會從 mesh 網路流向 MQTT,不會反向傳送。 請在接收端閘道上啟用下行功能。 -- 頻道不符 — 兩個閘道必須使用相同頻道且具備相同的 PSK。 不符時,訊息將以不同金鑰加密,導致對方收到的內容為亂碼。 +- 檢查上行/下行設定 — 若僅啟用上行,訊息只會從 mesh 網路流向 MQTT,不會反向傳送。請在接收端閘道上啟用下行功能。 +- 頻道不符 — 兩個閘道必須使用相同頻道且具備相同的 PSK。不符時,訊息將以不同金鑰加密,導致對方收到的內容為亂碼。 - **Topic mismatch** — both gateways must use exactly the same root topic. Setting a region rewrites a default root to `msh/` (for example `msh/US`), so gateways in different regions do not meet until you give both the same explicit root. - **Ignore MQTT is on** — in a region with a duty-cycle limit, the radio turns on **Ignore MQTT** (LoRa config, **Advanced**) when you set the region, and then drops every packet that reached it via MQTT. Turn it off on the receiving nodes, not only on the gateway. - **Ok to MQTT is off** — on a public broker a gateway uplinks other nodes' packets only when the sending node has **Ok to MQTT** (LoRa config, **Advanced**) on. Your own traffic bridges either way; your neighbors' does not until they opt in. diff --git a/docs/zh-rTW/user/node-metrics.md b/docs/zh-rTW/user/node-metrics.md index 2ab49c01a5..47ccff45e1 100644 --- a/docs/zh-rTW/user/node-metrics.md +++ b/docs/zh-rTW/user/node-metrics.md @@ -1,8 +1,7 @@ --- title: Node Metrics -parent: User Guide nav_order: 5 -last_updated: 2026-09-09 +last_updated: 2026-09-28 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -152,7 +151,7 @@ Traceroute shows the path a message takes through the mesh: ### Reading Traceroute Results -A traceroute is a round trip, so each saved result carries a hop count in each direction — **Forward Hops** and **Return Hops** — and the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. On Android that view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. +A traceroute is a round trip, so each saved result carries a hop count in each direction, **Forward Hops** and **Return Hops**, plus the **Round Trip** time in seconds. A result marked **Direct** reached the target with no relay in between. Tap a result to read the route traced toward the destination and the route traced back to you, with the SNR of every hop. That view offers **View on map**, which draws the same path, as long as the start and destination nodes have both shared a position. A result marked **No Response** means the target never answered. It may be out of range, asleep, or configured not to reply. Wait for the 30-second cooldown to clear and try again; if it keeps failing, send a direct message first to confirm the node is reachable at all. diff --git a/docs/zh-rTW/user/nodes.md b/docs/zh-rTW/user/nodes.md index 31c953b696..8015225af4 100644 --- a/docs/zh-rTW/user/nodes.md +++ b/docs/zh-rTW/user/nodes.md @@ -1,8 +1,7 @@ --- title: 節點 -parent: User Guide nav_order: 4 -last_updated: 2026-09-11 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,32 +14,36 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indicator | Meaning | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Green last-heard time | Node heard within the last 2 hours | +| Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | +| ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -58,18 +61,18 @@ Nodes can be configured with different roles that affect their mesh behavior: | 感測器 | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. @@ -78,19 +81,19 @@ Most users should keep the default **Client** role. Consider a different role wh Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | Shown for | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | -| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | -| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | -| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | -| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -107,10 +110,10 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. @@ -118,26 +121,44 @@ Sharing your own contact this way marks it as verified in person, so whoever imp To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles -| 過濾器 | 描述說明 | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | -| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 過濾器 | 描述說明 | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | 描述說明 | | --------------------------------------------- | ------------------------------------------------------------------ | @@ -146,14 +167,14 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. @@ -173,7 +194,19 @@ Inline status indicators show key metrics at a glance: | 最近一次收到排序 | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | 距離 | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Hardware support status + +When a node's hardware is recognized, the detail view names the device and marks its support status, taken from the Meshtastic device registry rather than the app: + +| Mark | 含義 | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Rosette (green) | **Supported** — hardware the Meshtastic project actively supports | +| Wrench (sky blue) | **Independent maker hardware** — built and tested by an independent maker, shown as its own rung whether or not the project has promoted it yet | +| Unverified (red) | **Supported by Meshtastic Community** — hardware the project does not actively support, including legacy boards | + +The label always accompanies the mark, so the status is never conveyed by colour alone. + +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -181,12 +214,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/zh-rTW/user/notifications.md b/docs/zh-rTW/user/notifications.md new file mode 100644 index 0000000000..676d4e50eb --- /dev/null +++ b/docs/zh-rTW/user/notifications.md @@ -0,0 +1,72 @@ +--- +title: 通知 +nav_order: 18 +last_updated: 2026-09-28 +description: What each Meshtastic notification is for, how to silence one kind without the others, and what you can do from a message notification or a watch. +aliases: + - notifications + - notification-channels + - wear-os + - smartwatch +--- + +# 通知 + +Meshtastic posts a notification when something on the mesh needs you while the app is out of sight: a message, a new node, a low battery, or a notice from your node. Each kind is its own Android notification category, so you can silence one and keep the rest. + +## Notification categories + +To see the categories, tap **App Notifications** in the **Information** section of **Settings**. It opens Android's notification settings for Meshtastic, where the categories sit in three groups. + +| Group | Category | Posted for | Tapping it opens | +| ------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| 訊息 | 私訊通知 | A message sent directly to you | The conversation | +| 訊息 | 廣播訊息通知 | A message on one of your channels | The channel | +| 訊息 | 航點通知 | A waypoint shared on the mesh, or a geofence crossing | The waypoint on the map | +| 訊息 | 警告信息 | A critical alert from a node | The conversation | +| 網狀網路 (Mesh) | 新節點通知 | A node heard for the first time | The node's details | +| 網狀網路 (Mesh) | Mesh invitation notifications | An invitation to join a nearby mesh | Local Mesh Discovery | +| 網狀網路 (Mesh) | 低電量通知(收藏節點) | A favorite node's battery running low | The node's details | +| 裝置 | 服務通知 | The connection to your node while the app runs in the background | The app | +| 裝置 | 低電量通知 | Your node's battery running low | The node's details | +| 裝置 | Radio notifications | Notices from your node, such as key verification requests and security warnings | The app | +| 裝置 | Update and connection notifications | A firmware update for your node, or a problem reconnecting to it | Firmware update, or Connections | + +By default, direct messages, alerts and radio notices pop up on screen, mesh invitations and the service notification arrive without a sound, and the rest make a sound. Android keeps whatever you change on a category, and the app cannot change it back. + +Alert notifications play an alarm sound. To let them through Do Not Disturb as well, open the category and allow it to override Do Not Disturb, which is the step [Getting Started](onboarding#critical-alerts-permission) offers during setup. + +The service notification stays in the shade while Meshtastic is connected in the background. It shows the connection state, and also shows progress while a firmware update or a preset scan runs. + +> ℹ️ **Note:** The Desktop app has no notification categories. It switches notifications from its own settings instead, described in [Notification Preferences](desktop#notification-preferences). + +## When a message notifies you + +One notification collects each conversation's recent messages. Whether a new message adds to it depends on where you are: + +- **Reading that conversation:** nothing is posted, since the message is already on screen. +- **In the app, on another screen:** the notification updates without a sound. +- **Anywhere else:** the notification updates and alerts you as its category is set to. + +Muting a conversation or a channel stops its notifications, except for a message that mentions you. Muting a node stops its notifications even when it mentions you. How to mute is in [Messages & Channels](messages-and-channels). A critical alert still alerts you while you are reading its conversation, and only muting silences it. + +## Acting from a notification + +A message notification has three actions: + +- **Reply** sends a message to that conversation without opening the app. The notification then updates to show your reply. +- **Mark as read** clears the conversation's unread count and dismisses the notification. +- **👍** reacts to the newest message with a thumbs-up. + +On Android 11 and newer, a message notification can also open as a floating bubble, described in [Conversation Bubbles](messages-and-channels#conversation-bubbles). + +## On a watch + +A Wear OS watch paired with your phone shows every Meshtastic notification except the service notification, which Android keeps on the phone because it never goes away. A message notification keeps its actions on the watch, and **Reply** there can offer suggested replies. The watch's own notification settings decide whether Meshtastic appears on it at all. + +## 相關主題 + +- [Messages & Channels](messages-and-channels) — muting conversations and nodes, bubbles, and message states +- [Getting Started](onboarding) — the notification and critical alert steps of setup +- [Firmware Updates](firmware) — the screen an update notification opens +- [Local Mesh Discovery](discovery) — what a mesh invitation offers diff --git a/docs/zh-rTW/user/onboarding.md b/docs/zh-rTW/user/onboarding.md index 61416885cd..83bfc407d0 100644 --- a/docs/zh-rTW/user/onboarding.md +++ b/docs/zh-rTW/user/onboarding.md @@ -1,146 +1,7 @@ --- title: 新手入門 -parent: User Guide -nav_order: |- - 1. **Overview** - - Meshtastic is a project that allows you to use inexpensive LoRa radios as a long range, off-grid, decentralized communication platform. These radios, combined with readily available and very affordable microcontrollers like the ESP32, nRF52, and RP2040, create a network that can be used to send text messages, share locations, and more without relying on cellular or WiFi infrastructure. It's perfect for hiking, camping, or any situation where you need to stay connected beyond the reach of traditional networks. - - 2. **Key Components** - - * **LoRa Radios:** These radios provide the long-range communication capabilities. The RAK Wireless modules are a popular choice. - * **Microcontrollers:** The brains of the operation. ESP32, nRF52, and RP2040 are commonly used due to their low cost and capabilities. - * **GPS (Optional):** For location sharing. Many devices, like the T-Beam, have built-in GPS. - * **Battery (Optional):** For portable use. - * **Enclosure (Optional):** To protect the hardware. - - 3. **Popular Devices** - - * **T-Beam:** A popular board with ESP32, LoRa, and GPS. - * **Heltec WiFi LoRa 32:** Another popular ESP32-based board with LoRa. - * **LilyGo Boards:** LilyGo offers a variety of ESP32 and nRF52 based boards with LoRa. - - 4. **Features** - - * **Text Messaging:** Send and receive text messages over the LoRa network. - * **Location Sharing:** Share your location with other users on the network (requires GPS). - * **Encryption:** Messages can be encrypted for privacy. - * **Mesh Networking:** The network automatically routes messages through other nodes to reach the destination. - * **Off-Grid Communication:** No cellular or WiFi required. - * **Channel Settings:** Customize channel settings for different regions and use cases (Long Fast, Mid Slow, etc.). - - 5. **Software & Firmware** - - * **Meshtastic Firmware:** The core software that runs on the microcontrollers. - * **Meshtastic Mobile App:** For configuring and interacting with the devices. Available on Android and iOS. - * **CLI (Command Line Interface):** For advanced configuration and debugging. - * **API (Application Programming Interface):** For integrating Meshtastic with other applications. - - 6. **Hardware Setup** - - * **Flashing Firmware:** You'll need to flash the Meshtastic firmware onto your microcontroller. This is typically done using a USB connection and a flashing tool. - * **Connecting Peripherals:** Connect the LoRa radio and any other peripherals (GPS, battery) to the microcontroller. - * **Antenna:** Attach an appropriate antenna to the LoRa radio. - - 7. **Configuration** - - * **Region Settings:** Configure the correct region settings for your location. - * **Channel Settings:** Choose a channel or create a custom channel. - * **Encryption:** Enable encryption for secure communication. - * **Power Settings:** Adjust power settings to optimize battery life. - - 8. **Technical Details** - - * **LoRa Modulation:** Uses LoRa modulation for long-range communication. - * **Frequency Bands:** Operates on various frequency bands depending on the region (e.g., 915 MHz in North America, 868 MHz in Europe). - * **Microcontroller Interfaces:** Uses various interfaces for communication between the microcontroller and peripherals, including GPIO, USB, UART, SPI, and I2C. - * **BLE (Bluetooth Low Energy):** Used for initial configuration and communication with the mobile app. - * **WiFi:** Some devices support WiFi for OTA (Over-The-Air) firmware updates. - * **MQTT:** Supports MQTT for integration with other systems. - - 9. **Use Cases** - - * **Hiking and Camping:** Stay connected with your group in areas without cellular coverage. - * **Emergency Communication:** Provide a backup communication system in case of emergencies. - * **Disaster Relief:** Establish communication networks in areas affected by disasters. - * **Rural Communication:** Connect communities in remote areas. - * **IoT Applications:** Use Meshtastic for various IoT applications that require long-range communication. - - 10. **Resources** - - * **Meshtastic Website:** [https://meshtastic.org/](https://meshtastic.org/) - * **Meshtastic Documentation:** [https://meshtastic.org/docs/](https://meshtastic.org/docs/) - * **Meshtastic Forums:** [https://meshtastic.discourse.group/](https://meshtastic.discourse.group/) - 1. **概觀** - - Meshtastic 是一個專案,讓你可以使用便宜的 LoRa 無線電作為長距離、離線、去中心化的通訊平台。這些無線電,結合了容易取得且非常實惠的微控制器,像是 ESP32、nRF52 和 RP2040,創建了一個網路,可以用來傳送簡訊、分享位置等等,而不需要依賴行動網路或 WiFi 基礎設施。它非常適合健行、露營,或任何你需要保持連線,但又超出傳統網路覆蓋範圍的情況。 - - 2. **主要組件** - - * **LoRa 無線電:** 提供長距離通訊能力。 RAK Wireless 模組是一個很受歡迎的選擇。 - * **微控制器:** 運作的大腦。 ESP32、nRF52 和 RP2040 因為它們的低成本和功能而被廣泛使用。 - * **GPS (可選):** 用於位置分享。 許多裝置,像是 T-Beam,都有內建 GPS。 - * **電池 (可選):** 用於攜帶型使用。 - * **外殼 (可選):** 保護硬體。 - - 3. **熱門裝置** - - * **T-Beam:** 一個受歡迎的板子,具有 ESP32、LoRa 和 GPS。 - * **Heltec WiFi LoRa 32:** 另一個受歡迎的基於 ESP32 的板子,具有 LoRa。 - * **LilyGo Boards:** LilyGo 提供各種基於 ESP32 和 nRF52 的板子,具有 LoRa。 - - 4. **功能** - - * **簡訊傳輸:** 透過 LoRa 網路傳送和接收簡訊。 - * **位置分享:** 與網路上其他使用者分享你的位置 (需要 GPS)。 - * **加密:** 可以加密訊息以保護隱私。 - * **網狀網路:** 網路會自動透過其他節點路由訊息,以到達目的地。 - * **離線通訊:** 不需要行動網路或 WiFi。 - * **頻道設定:** 客製化不同地區和使用案例的頻道設定 (Long Fast、Mid Slow 等)。 - - 5. **軟體與 Firmware** - - * **Meshtastic Firmware:** 在微控制器上執行的核心軟體。 - * **Meshtastic Mobile App:** 用於配置和與裝置互動。 可在 Android 和 iOS 上使用。 - * **CLI (Command Line Interface):** 用於進階配置和除錯。 - * **API (Application Programming Interface):** 用於將 Meshtastic 與其他應用程式整合。 - - 6. **硬體設定** - - * **刷入 Firmware:** 你需要將 Meshtastic firmware 刷入你的微控制器。 這通常是使用 USB 連線和刷入工具來完成的。 - * **連接週邊設備:** 將 LoRa 無線電和任何其他週邊設備 (GPS、電池) 連接到微控制器。 - * **天線:** 將適當的天線連接到 LoRa 無線電。 - - 7. **配置** - - * **區域設定:** 為你的位置配置正確的區域設定。 - * **頻道設定:** 選擇一個頻道或創建一個自定義頻道。 - * **加密:** 啟用加密以進行安全通訊。 - * **電源設定:** 調整電源設定以優化電池壽命。 - - 8. **技術細節** - - * **LoRa 調變:** 使用 LoRa 調變進行長距離通訊。 - * **頻率範圍:** 根據地區在不同的頻率範圍上運作 (例如,北美為 915 MHz,歐洲為 868 MHz)。 - * **微控制器介面:** 使用各種介面在微控制器和週邊設備之間進行通訊,包括 GPIO、USB、UART、SPI 和 I2C。 - * **BLE (Bluetooth Low Energy):** 用於初始配置和與行動應用程式的通訊。 - * **WiFi:** 某些裝置支援 WiFi 用於 OTA (Over-The-Air) firmware 更新。 - * **MQTT:** 支援 MQTT 用於與其他系統整合。 - - 9. **使用案例** - - * **健行和露營:** 在沒有行動網路覆蓋的地區與你的團隊保持聯繫。 - * **緊急通訊:** 在緊急情況下提供備份通訊系統。 - * **災害救援:** 在受災害影響的地區建立通訊網路。 - * **農村通訊:** 連接偏遠地區的社群。 - * **IoT 應用:** 將 Meshtastic 用於各種需要長距離通訊的 IoT 應用。 - - 10. **資源** - - * **Meshtastic Website:** [https://meshtastic.org/](https://meshtastic.org/) - * **Meshtastic Documentation:** [https://meshtastic.org/docs/](https://meshtastic.org/docs/) - * **Meshtastic Forums:** [https://meshtastic.discourse.group/](https://meshtastic.discourse.group/) -last_updated: 2026-08-30 +nav_order: 1 +last_updated: 2026-09-28 description: First-launch setup — permissions, onboarding flow, and next steps after connecting your radio. aliases: - first-launch @@ -193,6 +54,8 @@ Meshtastic also uses your location for: - Calculating distances to other nodes - Sharing your GPS coordinates with other mesh members (if enabled) +On Android 12 and newer, choose **Precise** to share your position with the mesh: an approximate grant still shows you on the map, but position sharing needs precise location. + Grant **"While using the app"**. The app does not request background location — `ACCESS_BACKGROUND_LOCATION` is not in its manifest — so Android will not offer an "Always" option, and position updates happen while the app is in the foreground or running its foreground service. Declining leaves the rest of the app working: on Android 12 and newer, Bluetooth is unaffected and only the map position and position sharing are disabled. On Android 11 and older, Bluetooth scanning also stops, because that is the permission Android gates it behind — and system **Location Services** must also be switched on for a scan to return anything. diff --git a/docs/zh-rTW/user/settings-module-admin.md b/docs/zh-rTW/user/settings-module-admin.md index d256b9f5fc..bda46028b0 100644 --- a/docs/zh-rTW/user/settings-module-admin.md +++ b/docs/zh-rTW/user/settings-module-admin.md @@ -1,8 +1,7 @@ --- title: Settings — Modules & Admin -parent: User Guide nav_order: 8 -last_updated: 2026-09-16 +last_updated: 2026-09-28 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +13,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -30,39 +29,39 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node: the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**. External Notification may still restart the node for some changes, while a Mesh Beacon change applies without a restart. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. -| 設定 | 描述說明 | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 啟用MQTT服務器 | Toggle MQTT bridge | -| 地址 | MQTT broker address | -| 使用者名稱 | Authentication username | -| 密碼 | Authentication password | -| 加密已啟用 | Encrypt MQTT payloads | -| JSON輸出已啟用 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | -| TLS已啟用 | Use secure connection | -| 根話題 | Base MQTT topic path | -| 啟用對客戶端的代理 | Let a connected phone carry the node's MQTT traffic, instead of the node reaching the broker itself | -| MQTT proxy on this phone | The phone-side half of **Proxy to client enabled**: whether this phone acts as that relay. See [MQTT](mqtt) | -| 地圖報告 | Publish position to the public map — see the Map reporting group that follows | +| 設定 | 描述說明 | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 啟用MQTT服務器 | Toggle MQTT bridge | +| 地址 | MQTT broker address | +| 使用者名稱 | Authentication username | +| 密碼 | Authentication password | +| 加密已啟用 | Encrypt MQTT payloads | +| JSON輸出已啟用 | Publish and consume MQTT messages as JSON. Marked deprecated in the protobuf schema, but it is still the only toggle for this behavior and the firmware still honors it | +| TLS已啟用 | Use secure connection | +| 根話題 | Base MQTT topic path | +| 啟用對客戶端的代理 | Let the connected app carry the node's MQTT traffic, instead of the node reaching the broker itself | +| MQTT proxy in this app | The app-side half of **Proxy to client enabled**: whether this app acts as that relay. See [MQTT](mqtt) | +| 地圖報告 | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed _Consent to Share Unencrypted Node Data -via MQTT_, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT_, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: -| 設定 | 描述說明 | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 我同意。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | -| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | -| 地圖報告間隔(秒) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | +| 設定 | 描述說明 | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 我同意。 | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | +| Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads _±_ the resulting distance, in your own units | +| Map Publish Interval | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +75,9 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout - 超時 | How long to wait before considering an incoming message complete | | 覆蓋控制台序列埠 | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -94,11 +93,11 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | 輸出振動(GPIO) | Pin the vibration motor is wired to | | 使用PWM調製的蜂鳴 | Drive the buzzer with PWM, which allows tones rather than a single pitch | | 使用 I2S 控制蜂鳴器 | Send the alert through an I2S audio output instead | -| 輸出持續時間(毫秒) | How long a single alert lasts | -| 通知逾時時間(秒) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | +| GPIO Output Duration | How long a single alert lasts | +| 持續提醒逾時 | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | 鈴聲 | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +112,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -122,38 +121,38 @@ Buffers messages for nodes that were temporarily offline, then replays them when Automated range testing tool for evaluating link quality between nodes. When enabled, the node periodically transmits test messages with incrementing counters. A receiver node logs these messages, allowing you to walk or drive away and later analyze at what distance messages stopped arriving. -| 設定 | 描述說明 | -| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -| 啟用範圍測試 | Activate range testing | -| 訊息發送間隔(秒) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| 將 .CSV 保存到內部儲存空間(僅限ESP32) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| 設定 | 描述說明 | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| 啟用範圍測試 | Activate range testing | +| 傳送間隔 | Time between test transmissions, chosen from a dropdown of fixed intervals | +| 將 .CSV 保存到內部儲存空間(僅限ESP32) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). Each of the four metric groups has its own enable toggle and its own interval, so you can report battery health often and sensors rarely. -| 設定 | 描述說明 | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 傳送裝置遙測資料 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | -| 裝置資訊更新間隔 | How often to report battery, uptime and channel utilization | -| 啟用環境資訊模組 | Report the attached environment sensors | -| 環境資訊更新間隔 | How often to report them | -| 在螢幕上顯示環境資訊 | Also show these readings on the device's own display | -| 環境指標以華氏溫度顯示 | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | -| 啟用空氣品質模組 | Report particulate and CO₂ sensor data | -| 空氣品質資訊更新間隔 | How often to report them | -| 啟用電池資訊模組 | Report the per-channel voltage and current readings | -| 電源資訊更新間隔 | How often to report them | -| 在螢幕上顯示電量資訊 | Also show power readings on the device's display | +| 設定 | 描述說明 | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 傳送裝置遙測資料 | Master toggle for device metrics. Only shown on firmware 2.7.12 and newer | +| 裝置資訊更新間隔 | How often to report battery, uptime and channel utilization | +| 啟用環境資訊模組 | Report the attached environment sensors | +| 環境資訊更新間隔 | How often to report them | +| 在螢幕上顯示環境資訊 | Also show these readings on the device's own display | +| 環境指標以華氏溫度顯示 | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| 啟用空氣品質模組 | Report particulate and CO₂ sensor data | +| 空氣品質資訊更新間隔 | How often to report them | +| 啟用電池資訊模組 | Report the per-channel voltage and current readings | +| 電源資訊更新間隔 | How often to report them | +| 在螢幕上顯示電量資訊 | Also show power readings on the device's display | See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | 設定 | 描述說明 | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | @@ -166,7 +165,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | 啟用上下選擇輸入 | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +181,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | 設定 | 描述說明 | | --------- | --------------------------------------------------------------- | @@ -194,19 +193,19 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | 允許未定義腳位連接 | Allow access to any GPIO pin (security risk) | | 可用腳位 | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. | 設定 | 描述說明 | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 啟用鄰居資訊 | Activate neighbor broadcasting | -| 更新間隔(秒) | How often to broadcast neighbor list | +| GPS 輪詢間隔 | How often to broadcast neighbor list | | 通過Lora無線電傳輸 | Also broadcast neighbor info over LoRa, not just MQTT/phone. Unavailable on a channel using the default key and name | See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,49 +215,49 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | 目前 | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. -| 設定 | 描述說明 | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| 啟用偵測感測器 | Activate detection sensor | -| 螢幕的 GPIO 腳位 | GPIO pin connected to sensor | -| 偵測觸發類型 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | -| 使用輸入上拉模式 | Enable the pin's internal pull-up resistor | -| 最短廣播間隔 (秒) | Minimum time between alert broadcasts | -| 狀態廣播間隔 (秒) | Periodic state broadcast interval | -| 告警訊息發送提示音 | Include bell character in alerts | -| 顯示名稱 | Custom name for this sensor | +| 設定 | 描述說明 | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| 啟用偵測感測器 | Activate detection sensor | +| 螢幕的 GPIO 腳位 | GPIO pin connected to sensor | +| 偵測觸發類型 | How the pin's state maps to a detection event (e.g. active high/low, edge-triggered) | +| 使用輸入上拉模式 | Enable the pin's internal pull-up resistor | +| Minimum time between detection broadcasts | Minimum time between alert broadcasts | +| 狀態廣播間隔 | Periodic state broadcast interval | +| 告警訊息發送提示音 | Include bell character in alerts | +| 顯示名稱 | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. -| 設定 | 描述說明 | -| -------------------- | ----------------------------------------------------------------------------------------------------------------- | -| 已啟用人流計數(Paxcount) | Activate people counting | -| 更新間隔(秒) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | -| BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | +| 設定 | 描述說明 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| 已啟用人流計數(Paxcount) | Activate people counting | +| GPS 輪詢間隔 | How often to report counts | +| WiFi Threshold (dBm) | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | +| BLE Threshold (dBm) | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -279,32 +278,33 @@ Remotely configure nodes that share your admin key: **Settings → Administration** holds five one-shot actions, each behind a confirmation dialog: -| 動作 | What it does | -| -------- | ------------------------------------------------------------------------------------------------------ | -| Set time | Sends your phone's clock to the radio | -| 重新開機 | Restarts the radio | -| 關機 | Powers the radio down | -| 恢復出廠設置 | Returns every setting to its factory default | -| 重設節點資料庫 | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| 動作 | What it does | +| -------- | ---------------------------------------------------------------------------------------------- | +| Set time | Sends your phone's clock to the node | +| 重新開機 | Restarts the node | +| 關機 | Powers the node down | +| 恢復出廠設置 | Returns every setting to its factory default | +| 重設節點資料庫 | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### 備份與還原 -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with -**Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with +**Export configuration**, and reads a saved file back in with **Import configuration**. +Traffic Management settings are exported but not applied on import. Export before a factory +reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### 進階 **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### 清除節點資料庫 -Prunes nodes from your node database — from the app's copy _and_ from the radio's own, so this is +Prunes nodes from your node database — from the app's copy _and_ from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as _Selections are additive_. @@ -314,7 +314,7 @@ it as _Selections are additive_. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,12 +324,39 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings 應用程式設定 -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. + It needs precise location: with approximate location allowed, turning it on asks for precise. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### About(關於) @@ -352,7 +379,7 @@ may redistribute under the same license. Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -360,6 +387,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/zh-rTW/user/settings-radio-user.md b/docs/zh-rTW/user/settings-radio-user.md index 21f56f6a8c..88c609bc6e 100644 --- a/docs/zh-rTW/user/settings-radio-user.md +++ b/docs/zh-rTW/user/settings-radio-user.md @@ -1,9 +1,8 @@ --- title: Settings — Radio & User -parent: User Guide nav_order: 7 -last_updated: 2026-09-11 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-28 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 設定 - radio-config @@ -13,14 +12,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -36,20 +35,20 @@ Settings use standard preference controls — dropdowns, toggles, and sliders: On **Settings → User**. -| 設定 | 描述說明 | -| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 長名稱 | Your display name (up to 39 characters) | -| 簡短名稱 | 4-character abbreviated name | -| 狀態訊息 | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | -| 不接收訊息 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| 領有執照的業餘無線電台 (HAM) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| 設定 | 描述說明 | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 長名稱 | Your display name (up to 39 characters) | +| 簡短名稱 | 4-character abbreviated name | +| 狀態訊息 | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| 不接收訊息 | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose **Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. @@ -66,7 +65,7 @@ On **Settings → Device configuration → Device**. | ## Rebroadcast Mode轉發廣播模式 | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | 節點資訊廣播間隔 | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | 雙擊觸發按鈕功能 | Treat a double tap as a button press | Disabled | -| 三擊執行 Ad Hoc Ping | Send an ad-hoc position ping on a triple click | 已停用 | +| 三擊執行 Ad Hoc Ping | Send an ad-hoc position ping on a triple click | 已啟用 | | LED 心跳指示 | Blink the status LED periodically | 已啟用 | | 時區 | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -75,32 +74,39 @@ On **Settings → Device configuration → Device**. On **Settings → LoRa**. -| 設定 | 描述說明 | 默認 | -| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 地區 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | -| 預設配置 | Speed/range tradeoff | LongFast | -| 中繼次數 | Maximum retransmit hops | 3 | -| 傳輸功率 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| 手動設定頻率 | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | -| 使用預設值 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 擴頻因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | -| 編碼率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| 頻寬 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | -| 頻率槽 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | -| 已啟用發射 | Turning this off makes the node receive-only | On | -| 覆蓋工作週期/佔空比 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | -| 忽略 MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | -| 允許轉發至 MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | -| 接收增益提升 | Extra receive gain on SX126x radios; costs a little current | Off | -| 停用PA風扇 | Turn off the power-amplifier fan on hardware that has one | Off | +| 設定 | 描述說明 | 默認 | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| 地區 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | +| 預設配置 | Speed/range tradeoff | LongFast | +| 中繼次數 | Maximum retransmit hops | 3 | +| Transmit Power (dBm) | Transmission power; 0 = max allowed for region | 0 (region max) | +| 手動設定頻率 | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| 使用預設值 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | +| Coding Rate Override | Preset mode only, firmware 2.7.18 or newer: raises the preset's coding rate for more error correction while keeping its bandwidth and spread factor. Only rates above the preset's own are offered, and a preset already at 4/8 has none. Nodes on the same preset still hear each other, but every packet takes longer on air and uses more of the duty cycle. Switching to a preset that already meets the override resets it to the preset default | Preset default | +| 擴頻因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | +| 編碼率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | +| 頻寬 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | +| 頻率槽 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | +| 已啟用發射 | Turning this off makes the node receive-only | On | +| 覆蓋工作週期/佔空比 | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | +| 忽略 MQTT | Drop packets that arrived from MQTT rather than over the air. The firmware turns this on for you whenever you set a region that has a duty-cycle limit — the EU bands, Thailand, and Ukraine 433 | Off, until you set a duty-cycle-limited region | +| 允許轉發至 MQTT | Allow your packets to be forwarded to MQTT by gateways | Off | +| 接收增益提升 | Extra receive gain on SX126x radios; costs a little current | Off | +| 停用PA風扇 | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals _below_ the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -120,13 +126,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as _Long Fast_ and _Short Turbo_. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### 顯示設置 -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. -| 設定 | 描述說明 | -| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 螢幕開啟持續時間 | How long the display stays lit before sleeping | -| 輪播間隔 | How often the radio cycles between screens on its own | -| 顯示模式 | Screen layout/density used by the firmware | -| 顯示單位 | Metric or Imperial on the radio's screen | -| 使用12小時制 | Show the radio's clock as 12-hour rather than 24-hour | -| 粗體字 | Draw the screen's heading text in bold | -| 翻轉畫面 | Rotate the display 180° for an inverted mounting | -| OLED 類型 | Auto, SSD1306, SH1106, SH1107 | -| 輕觸或移動喚醒 | Light the screen when the radio is tapped or moved | -| 羅盤朝向 | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | -| 始終指向北方 | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | +| 設定 | 描述說明 | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 螢幕開啟持續時間 | How long the display stays lit before sleeping | +| 輪播間隔 | How often the node cycles between screens on its own | +| 顯示模式 | Screen layout/density used by the firmware | +| 顯示單位 | Metric or Imperial on the node's screen | +| 使用12小時制 | Show the node's clock as 12-hour rather than 24-hour | +| 粗體字 | Draw the screen's heading text in bold | +| 翻轉畫面 | Rotate the display 180° for an inverted mounting | +| OLED 類型 | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| 輕觸或移動喚醒 | Light the screen when the node is tapped or moved | +| 羅盤朝向 | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | +| 始終指向北方 | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### 位置設定 On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | 設定 | 描述說明 | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | GPS 模式(實體硬體) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS 輪詢間隔 | How often the radio asks its GPS for a fix | +| GPS 輪詢間隔 | How often the node asks its GPS for a fix | | 廣播間隔 | How often the position is shared with the mesh | | 智慧定位 | Broadcast based on movement rather than purely on the clock | | 智慧間隔 | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | 設定 | 描述說明 | | -------------------------------------- | --------------------------------------------------------------- | -| 啟用省電模式 | Let the radio sleep aggressively between activity | +| 啟用省電模式 | Let the node sleep aggressively between activity | | 電源中斷時關機 | Power the device down after external power disappears | | 超深度睡眠時長 | How long the deepest sleep state lasts | -| 最小喚醒時間 | The shortest time the radio stays awake once woken | +| 最小喚醒時間 | The shortest time the node stays awake once woken | | 藍牙等待持續時間 | How long to wait for a phone to connect before sleeping | | ADC 校正係數 | Turn on a manual correction for battery-voltage readings | | ADC乘數修正比率 | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### 網路配置 -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | 設定 | 描述說明 | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | 密碼 | 網路密碼 | | 啟用以太網 | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### 藍牙配置 -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | 設定 | 描述說明 | | ------ | ------------------------------------------------------------------------------------------------------ | @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | 設定 | 描述說明 | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 公鑰 | Your node's public key (read-only) | | 管理金鑰 | Keys permitted to administer this node remotely — up to three | -| 私鑰 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| 私鑰 | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | 重新產生私鑰 | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | 序列控制台 | Serial console over the Stream API | -| 啟用除錯日誌 API | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| 管理模式 | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| 啟用除錯日誌 API | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| 管理模式 | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | 備份金鑰 | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/zh-rTW/user/signal-meter.md b/docs/zh-rTW/user/signal-meter.md index 1573b18299..b43505ddee 100644 --- a/docs/zh-rTW/user/signal-meter.md +++ b/docs/zh-rTW/user/signal-meter.md @@ -1,6 +1,5 @@ --- title: How the Meshtastic Signal Meter Works -parent: User Guide nav_order: 15 last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. diff --git a/docs/zh-rTW/user/tak.md b/docs/zh-rTW/user/tak.md index b3f7292e92..0ba27e323b 100644 --- a/docs/zh-rTW/user/tak.md +++ b/docs/zh-rTW/user/tak.md @@ -1,8 +1,7 @@ --- title: TAK Integration -parent: User Guide nav_order: 10 -last_updated: 2026-09-11 +last_updated: 2026-09-28 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -105,6 +104,7 @@ Once configured: - Chat messages can bridge between mesh and TAK networks - Position updates flow bidirectionally between Meshtastic and TAK - TAK Tracker nodes broadcast PLI automatically — their positions appear on ATAK maps without any ATAK-side configuration +- Routes received from the mesh are also saved as a data package (`.zip`) in **Downloads**; import it in ATAK to add the route. On Android 9 and older the file goes to the app's own folder under `Android/data` instead > ℹ️ **Note:** TAK integration requires specific node roles. Standard client nodes don't automatically participate in TAK operations — though with **Mesh to CoT Converter** enabled they still appear on the ATAK map as contacts. diff --git a/docs/zh-rTW/user/telemetry-and-sensors.md b/docs/zh-rTW/user/telemetry-and-sensors.md index 62ec51ca69..da650886e2 100644 --- a/docs/zh-rTW/user/telemetry-and-sensors.md +++ b/docs/zh-rTW/user/telemetry-and-sensors.md @@ -1,8 +1,7 @@ --- title: Telemetry & Sensors -parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -43,11 +42,12 @@ Supported environmental sensors: ### Air Quality -| 感測器 | 公制(公里/公尺) | 備註 | -| -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| BME680 | Gas Resistance / IAQ | Volatile organic compounds | -| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | -| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| 感測器 | 公制(公里/公尺) | 備註 | +| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BME680 | Gas Resistance / IAQ | Volatile organic compounds | +| PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | +| SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +58,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | 感測器 | 公制(公里/公尺) | @@ -68,17 +70,18 @@ Both appear as info cards on the node detail screen, next to the other environme ### Weather and Other Readings -| 公制(公里/公尺) | 單位 | Where it appears | -| ------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | -| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | -| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | -| 輻射 | µR/h | Card and chart | -| 重量 | kg or lb | Card only — load cells, such as a beehive scale | -| 距離 | mm or in | Card only — water level, from a distance sensor | -| Dew point | °C or °F | Card only — computed from temperature and humidity | -| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | -| ADC voltage | 伏特 | Card and chart, up to eight raw analog channels | +| 公制(公里/公尺) | 單位 | Where it appears | +| ------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | +| Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | +| Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres | +| 輻射 | µR/h | Card and chart | +| 重量 | kg or lb | Card only — load cells, such as a beehive scale | +| 距離 | mm or in | Card only — water level, from a distance sensor | +| Dew point | °C or °F | Card only — computed from temperature and humidity | +| 1-Wire temperature | °C or °F | Card and chart, up to eight DS18B20-style probes | +| ADC voltage | 伏特 | Card and chart, up to eight raw analog channels | ## 電源計量資料 diff --git a/docs/zh-rTW/user/translate.md b/docs/zh-rTW/user/translate.md index ffe591eef6..da3bcd404e 100644 --- a/docs/zh-rTW/user/translate.md +++ b/docs/zh-rTW/user/translate.md @@ -1,6 +1,5 @@ --- title: Translate the App -parent: User Guide nav_order: 17 last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. diff --git a/docs/zh-rTW/user/units-and-locale.md b/docs/zh-rTW/user/units-and-locale.md index 87661dc47d..ecd69a3480 100644 --- a/docs/zh-rTW/user/units-and-locale.md +++ b/docs/zh-rTW/user/units-and-locale.md @@ -1,6 +1,5 @@ --- title: Units, Measurement & Locale -parent: User Guide nav_order: 16 last_updated: 2026-08-30 description: How the app formats temperature, distance, speed, and other measurements based on your device locale. @@ -20,7 +19,7 @@ The Meshtastic app automatically displays temperatures, distances, speeds, and t Meshtastic radios always transmit data in **metric units** (meters, °C, m/s, hPa, etc.). When the app receives this data, it converts and displays values in whatever unit system your device's locale specifies. -在 Android 上,量測單位偏好由系統的語言與地區設定決定。 在桌面版(JVM)上,應用程式使用 JVM 的預設 Locale。 +在 Android 上,量測單位偏好由系統的語言與地區設定決定。在桌面版(JVM)上,應用程式使用 JVM 的預設 Locale。 Units follow your device's **region**, not the display language. Plain languages — like **English** in the app's own Language setting or Android's per-app language — keep the region your device is set to. A choice that names a region of its own, like **English (Canada)**, overrides it and brings that region's units with it. On Android 16+, the system-wide **Measurement system** preference overrides the region for distance, speed, and the other measurements — but not for temperature, which keeps following the region. diff --git a/docs/zh-rTW/user/widget.md b/docs/zh-rTW/user/widget.md index 37e23e986b..bdb091e708 100644 --- a/docs/zh-rTW/user/widget.md +++ b/docs/zh-rTW/user/widget.md @@ -1,6 +1,5 @@ --- title: Home Screen Widget -parent: User Guide nav_order: 20 last_updated: 2026-08-30 description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 30b59945c8..5224130373 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,18 +1,19 @@ # Lanes are invoked by the workflows in .github/workflows (release.yml, -# promote.yml, play-listing.yml); `bundle exec fastlane lanes` lists them. +# promote.yml, play-listing.yml, play-rollout.yml); `bundle exec fastlane lanes` +# lists them. # Play credentials come from fastlane/play-store-credentials.json, written by # the workflow from the GOOGLE_PLAY_JSON_KEY secret and deleted afterwards. default_platform(:android) platform :android do - desc "Deploy a new version to the internal track on Google Play" - lane :internal do |options| - aab_path = build_google_release + desc "Upload a built Google release bundle to the internal track on Google Play. Pass aab:" + lane :upload_internal do |options| + UI.user_error!("Pass the bundle to upload as aab:") if options[:aab].to_s.empty? upload_to_play_store( track: 'internal', - aab: aab_path, + aab: options[:aab], release_status: 'completed', skip_upload_apk: true, skip_upload_metadata: true, @@ -49,31 +50,27 @@ platform :android do ) end - desc "Build the F-Droid release" - lane :fdroid_build do - gradle( - task: "assembleFdroidRelease", - properties: { - "android.injected.version.name" => ENV['VERSION_NAME'], - "android.injected.version.code" => ENV['VERSION_CODE'], - # Intentionally omit aboutLibraries.release — fdroid builds must use - # offlineMode so the output matches F-Droid's reproducible rebuild. - } - ) - end + desc "Write every release on a Play track (status, version codes, user fraction) as JSON. Pass track: out:. Reads only; the edit is discarded" + lane :play_track_releases do |options| + UI.user_error!("Pass track: and out:") if options[:track].to_s.empty? || options[:out].to_s.empty? - desc "Build the Google Release" - private_lane :build_google_release do - gradle( - task: "bundleGoogleRelease assembleGoogleRelease", - print_command: false, - properties: { - "android.injected.version.name" => ENV['VERSION_NAME'], - "android.injected.version.code" => ENV['VERSION_CODE'], - "aboutLibraries.release" => "true", - "meshtastic.disableAbiSplits" => "true" - } - ) - lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH] + require 'json' + require 'supply' + require 'supply/options' + require 'supply/reader' + + # Lane code runs in ./fastlane, and the Appfile's key path is relative to the project root. + Dir.chdir('..') do + Supply.config = FastlaneCore::Configuration.create(Supply::Options.available_options, { track: options[:track].to_s }) + track = Supply::Reader.new.track_meta + releases = (track&.releases || []).map do |release| + { + status: release.status, + version_codes: (release.version_codes || []).map(&:to_i), + user_fraction: release.user_fraction, + } + end + File.write(options[:out], JSON.pretty_generate(releases)) + end end end diff --git a/fastlane/README.md b/fastlane/README.md index f248380e91..bfb784449e 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -15,13 +15,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do ## Android -### android internal +### android upload_internal ```sh -[bundle exec] fastlane android internal +[bundle exec] fastlane android upload_internal ``` -Deploy a new version to the internal track on Google Play +Upload a built Google release bundle to the internal track on Google Play. Pass aab: ### android play_listing @@ -31,13 +31,13 @@ Deploy a new version to the internal track on Google Play Upload the store listing - title, descriptions, feature graphic, icon and screenshots - for every locale under fastlane/metadata/android. Touches no build or track. Dry-runs unless validate_only:false -### android fdroid_build +### android play_track_releases ```sh -[bundle exec] fastlane android fdroid_build +[bundle exec] fastlane android play_track_releases ``` -Build the F-Droid release +Write every release on a Play track (status, version codes, user fraction) as JSON. Pass track: out:. Reads only; the edit is discarded ---- diff --git a/fastlane/metadata/android/ar/changelogs/default.txt b/fastlane/metadata/android/ar/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/ar/changelogs/default.txt +++ b/fastlane/metadata/android/ar/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/be/changelogs/default.txt b/fastlane/metadata/android/be/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/be/changelogs/default.txt +++ b/fastlane/metadata/android/be/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/bg/changelogs/default.txt b/fastlane/metadata/android/bg/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/bg/changelogs/default.txt +++ b/fastlane/metadata/android/bg/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ca/changelogs/default.txt b/fastlane/metadata/android/ca/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/ca/changelogs/default.txt +++ b/fastlane/metadata/android/ca/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/default.txt b/fastlane/metadata/android/cs-CZ/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/cs-CZ/changelogs/default.txt +++ b/fastlane/metadata/android/cs-CZ/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/el-GR/changelogs/default.txt b/fastlane/metadata/android/el-GR/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/el-GR/changelogs/default.txt +++ b/fastlane/metadata/android/el-GR/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png index 4bbd2bb566..d49599bde0 100644 Binary files a/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png index 303ad5f300..be2de53263 100644 Binary files a/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png index 2517d4ec63..dc5fbd6bad 100644 Binary files a/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png index 434fda6c49..9b24c1911b 100644 Binary files a/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png index feccd2011c..a5dc561c48 100644 Binary files a/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png index 014ae9150f..9e9e714f5d 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png index b984ec5a8e..53c7435c22 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png index 3c2be8642c..a08422d30b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png index 3b02938ea9..220df45670 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png index 777cc6edb1..37573a9cf9 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png index d40a27b1a5..8a08b8879e 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png index 4363f09f85..b4499ce178 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png index 8a6b72dbdf..b80a84925b 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png index 3285efbe91..61bcc86915 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png index af089b648a..94efeab8a0 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png index b0e11455a9..b0b131acb5 100644 Binary files a/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png index ac4b5ad758..755abfc1ff 100644 Binary files a/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png index 7bfa40af8a..5ecd013127 100644 Binary files a/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png index 9be941ba86..9a62b1057b 100644 Binary files a/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png index dfd7e70ae1..ad9f73e01c 100644 Binary files a/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png index b9c8c9c06c..64a0aa2f56 100644 Binary files a/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png and b/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png index a9489adfcd..698ea86d59 100644 Binary files a/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png and b/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png index a74cd7f90e..950eb14ed0 100644 Binary files a/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png and b/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png index 1215787929..c31876f6e1 100644 Binary files a/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png and b/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png index ba7ccfbc5e..880e6f7b36 100644 Binary files a/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png and b/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/es-ES/changelogs/default.txt b/fastlane/metadata/android/es-ES/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/es-ES/changelogs/default.txt +++ b/fastlane/metadata/android/es-ES/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/et/changelogs/default.txt b/fastlane/metadata/android/et/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/et/changelogs/default.txt +++ b/fastlane/metadata/android/et/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/fi-FI/changelogs/default.txt b/fastlane/metadata/android/fi-FI/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/fi-FI/changelogs/default.txt +++ b/fastlane/metadata/android/fi-FI/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/fr-CA/changelogs/default.txt b/fastlane/metadata/android/fr-CA/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/fr-CA/changelogs/default.txt +++ b/fastlane/metadata/android/fr-CA/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/default.txt b/fastlane/metadata/android/fr-FR/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/default.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/gl-ES/changelogs/default.txt b/fastlane/metadata/android/gl-ES/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/gl-ES/changelogs/default.txt +++ b/fastlane/metadata/android/gl-ES/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/hr/changelogs/default.txt b/fastlane/metadata/android/hr/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/hr/changelogs/default.txt +++ b/fastlane/metadata/android/hr/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/default.txt b/fastlane/metadata/android/hu-HU/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/hu-HU/changelogs/default.txt +++ b/fastlane/metadata/android/hu-HU/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/is-IS/changelogs/default.txt b/fastlane/metadata/android/is-IS/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/is-IS/changelogs/default.txt +++ b/fastlane/metadata/android/is-IS/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/default.txt b/fastlane/metadata/android/it-IT/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/it-IT/changelogs/default.txt +++ b/fastlane/metadata/android/it-IT/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/iw-IL/changelogs/default.txt b/fastlane/metadata/android/iw-IL/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/iw-IL/changelogs/default.txt +++ b/fastlane/metadata/android/iw-IL/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/default.txt b/fastlane/metadata/android/ja-JP/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/default.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ko-KR/changelogs/default.txt b/fastlane/metadata/android/ko-KR/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/ko-KR/changelogs/default.txt +++ b/fastlane/metadata/android/ko-KR/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/lt/changelogs/default.txt b/fastlane/metadata/android/lt/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/lt/changelogs/default.txt +++ b/fastlane/metadata/android/lt/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/default.txt b/fastlane/metadata/android/nl-NL/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/nl-NL/changelogs/default.txt +++ b/fastlane/metadata/android/nl-NL/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/no-NO/changelogs/default.txt b/fastlane/metadata/android/no-NO/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/no-NO/changelogs/default.txt +++ b/fastlane/metadata/android/no-NO/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pl-PL/changelogs/default.txt b/fastlane/metadata/android/pl-PL/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/pl-PL/changelogs/default.txt +++ b/fastlane/metadata/android/pl-PL/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/default.txt b/fastlane/metadata/android/pt-BR/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/default.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/default.txt b/fastlane/metadata/android/pt-PT/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/pt-PT/changelogs/default.txt +++ b/fastlane/metadata/android/pt-PT/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ro/changelogs/default.txt b/fastlane/metadata/android/ro/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/ro/changelogs/default.txt +++ b/fastlane/metadata/android/ro/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/default.txt b/fastlane/metadata/android/ru-RU/changelogs/default.txt index 76874cc530..e21bd46978 100644 --- a/fastlane/metadata/android/ru-RU/changelogs/default.txt +++ b/fastlane/metadata/android/ru-RU/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. -Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases +Полное описание обновления: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sk/changelogs/default.txt b/fastlane/metadata/android/sk/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/sk/changelogs/default.txt +++ b/fastlane/metadata/android/sk/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sl/changelogs/default.txt b/fastlane/metadata/android/sl/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/sl/changelogs/default.txt +++ b/fastlane/metadata/android/sl/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sq/changelogs/default.txt b/fastlane/metadata/android/sq/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/sq/changelogs/default.txt +++ b/fastlane/metadata/android/sq/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sr/changelogs/default.txt b/fastlane/metadata/android/sr/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/sr/changelogs/default.txt +++ b/fastlane/metadata/android/sr/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/default.txt b/fastlane/metadata/android/sv-SE/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/sv-SE/changelogs/default.txt +++ b/fastlane/metadata/android/sv-SE/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/tr-TR/changelogs/default.txt b/fastlane/metadata/android/tr-TR/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/tr-TR/changelogs/default.txt +++ b/fastlane/metadata/android/tr-TR/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/uk/changelogs/default.txt b/fastlane/metadata/android/uk/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/uk/changelogs/default.txt +++ b/fastlane/metadata/android/uk/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/zh-CN/changelogs/default.txt b/fastlane/metadata/android/zh-CN/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/default.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/default.txt b/fastlane/metadata/android/zh-TW/changelogs/default.txt index 76874cc530..9bddca6cae 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/default.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/default.txt @@ -1,3 +1,3 @@ -Stability and reliability fixes. +Message notifications on Android Auto. Firmware updates show the installed and latest bootloader. A coding rate override on top of the modem preset. The MQTT proxy works on desktop, and so does the traceroute map. Tabs keep their state when you switch. Every notification gets its own channel. Many stability, privacy and data-correctness fixes. Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/feature/connections/README.md b/feature/connections/README.md index fea1b0e50f..f2057350cb 100644 --- a/feature/connections/README.md +++ b/feature/connections/README.md @@ -39,7 +39,6 @@ src/ │ │ ├── CurrentlyConnectedInfo.kt │ │ ├── DeviceList.kt / DeviceListItem.kt / DeviceSectionHeader.kt │ │ ├── DisconnectButton.kt -│ │ ├── EmptyStateContent.kt │ │ └── TransportSelector.kt │ └── di/ │ └── FeatureConnectionsModule.kt @@ -137,16 +136,11 @@ feature:connections graph TB :feature:connections[connections]:::kmp-feature :feature:connections -.-> :core:common - :feature:connections -.-> :core:data - :feature:connections -.-> :core:database :feature:connections -.-> :core:datastore :feature:connections -.-> :core:di - :feature:connections -.-> :core:domain :feature:connections -.-> :core:model :feature:connections -.-> :core:navigation - :feature:connections -.-> :core:prefs :feature:connections -.-> :core:resources - :feature:connections -.-> :core:service :feature:connections -.-> :core:ui :feature:connections -.-> :core:ble :feature:connections -.-> :core:network diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 696882eca6..6c3ab04859 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -23,17 +23,12 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) implementation(projects.core.datastore) implementation(projects.core.di) - implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.navigation) - implementation(projects.core.prefs) implementation(libs.meshtastic.protobufs) implementation(projects.core.resources) - implementation(projects.core.service) implementation(projects.core.ui) implementation(projects.core.ble) implementation(projects.core.network) diff --git a/feature/connections/detekt-baseline.xml b/feature/connections/detekt-baseline.xml index b1806ff2a9..e2679af7fa 100644 --- a/feature/connections/detekt-baseline.xml +++ b/feature/connections/detekt-baseline.xml @@ -5,10 +5,16 @@ Kdoc:TcpDiscoveryHelpers.kt:/** * Shared helpers for TCP device discovery logic used by both [CommonGetDiscoveredDevicesUseCase] and the * Android-specific variant. */ PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun BluetoothScanPreview PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun ConnectingDeviceInfoPreview + PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DemoModeSectionPreview PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceListItemPreview PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceSectionHeaderPreview PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DisconnectButtonPreview - PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun EmptyStateContentPreview + PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun TransportSelectorNoBluetoothPreview + PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun TransportSelectorNoUsbPreview PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun TransportSelectorPreview + UnnecessaryLaunchedEffect:ConnectionsNavigation.kt:LaunchedEffect + UnnecessaryLaunchedEffect:ConnectionsScreen.kt:LaunchedEffect + UnusedPrivateProperty:ScannerViewModel.kt:ScannerViewModel$private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase + UnusedPrivateProperty:ScannerViewModel.kt:ScannerViewModel$private val networkRepository: NetworkRepository
diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index 9e4473cf3b..f22535a1bd 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -71,6 +71,8 @@ class AndroidScannerViewModel( uiPrefs, firmwareRecoveryDataSource, bleScanner, + bluetoothSupported = bluetoothRepository.isSupported, + usbSupported = usbRepository.isSupported, ) { override fun requestBonding(entry: DeviceListEntry.Ble) { Logger.i { "Starting bonding for ${entry.device.address.anonymize}" } diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt index 8e7d74a300..4f8dd105a1 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt @@ -121,7 +121,8 @@ class AndroidGetDiscoveredDevicesUseCase( val recentList = args[5] as List val bleForUi = matchBleNodes(bondedBle, db) - val usbForUi = matchUsbNodes(usbDevices, showMock, showReplay, db) + val usbForUi = matchNodesByName(usbDevices, db) + val virtualForUi = matchNodesByName(virtualDeviceEntries(showMock, showReplay), db) val discoveredTcpForUi = matchDiscoveredTcpNodes(processedTcp, db, resolved, databaseManager) val discoveredTcpAddresses = processedTcp.map { it.fullAddress }.toSet() @@ -132,6 +133,7 @@ class AndroidGetDiscoveredDevicesUseCase( usbDevices = usbForUi, discoveredTcpDevices = discoveredTcpForUi, recentTcpDevices = recentTcpForUi, + virtualDevices = virtualForUi, ) } } @@ -155,12 +157,8 @@ class AndroidGetDiscoveredDevicesUseCase( } .sortedBy { it.name } - private suspend fun matchUsbNodes( - usbDevices: List, - showMock: Boolean, - showReplay: Boolean, - db: Map, - ): List = (usbDevices + virtualDeviceEntries(showMock, showReplay)).map { entry -> - entry.copy(node = findNodeByNameSuffix(entry.name, entry.fullAddress, db, databaseManager)) - } + private fun matchNodesByName(entries: List, db: Map): List = + entries.map { entry -> + entry.copy(node = findNodeByNameSuffix(entry.name, entry.fullAddress, db, databaseManager)) + } } diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 2fa7e8750f..69ce403944 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -51,6 +51,7 @@ import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.RadioController @@ -125,6 +126,9 @@ private fun untranslatedScanStartFailureMessage(reason: BleScanStartFailureReaso -> BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK } +private fun isBleAddress(address: String?): Boolean = + address?.firstOrNull().let { it == InterfaceId.BLUETOOTH.id || it == '!' } + private fun Duration.roundedUpWholeSeconds(): Long { val completeSeconds = inWholeSeconds return completeSeconds + if (this > completeSeconds.seconds) 1 else 0 @@ -150,6 +154,10 @@ open class ScannerViewModel( private val uiPrefs: UiPrefs, private val firmwareRecoveryDataSource: FirmwareRecoveryDataSource, private val bleScanner: BleScanner? = null, + /** False on hardware with no Bluetooth LE: the BLE pane is hidden and never selected or scanned. */ + val bluetoothSupported: Boolean = true, + /** False on hardware with no USB host: the USB pane is hidden and never selected. */ + val usbSupported: Boolean = true, ) : ViewModel() { // ── Mock / demo transport ───────────────────────────────────────────────────────────────── @@ -331,6 +339,10 @@ open class ScannerViewModel( val usbDevicesForUi: StateFlow> = discoveredDevicesFlow.map { it.usbDevices }.distinctUntilChanged().stateInWhileSubscribed(emptyList()) + /** Demo Mode entries, listed under whichever transport pane is showing. */ + val virtualDevicesForUi: StateFlow> = + discoveredDevicesFlow.map { it.virtualDevices }.distinctUntilChanged().stateInWhileSubscribed(emptyList()) + /** Discovered (NSD) TCP devices for the Connections device list, gated by the network-scan flag. */ val discoveredTcpDevicesForUi: StateFlow> = discoveredDevicesFlow.map { it.discoveredTcpDevices }.distinctUntilChanged().stateInWhileSubscribed(emptyList()) @@ -348,8 +360,28 @@ open class ScannerViewModel( // ── Current selection ──────────────────────────────────────────────────────────────────── - /** The currently-selected device address, or `null` when nothing is selected. */ - val selectedAddressFlow: StateFlow = radioInterfaceService.currentDeviceAddressFlow + /** + * The currently-selected device address, or `null` when nothing is selected. A BLE address on hardware without + * Bluetooth LE, or a serial address on hardware without USB host, reads as `null`: the radio service keeps it saved + * but it can never connect. + */ + val selectedAddressFlow: StateFlow = + if (bluetoothSupported && usbSupported) { + radioInterfaceService.currentDeviceAddressFlow + } else { + // Eager so the auto-scan checks that read `.value` see the masked address without a subscriber. + radioInterfaceService.currentDeviceAddressFlow + .map { it.takeUnless(::isUnsupportedAddress) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = + radioInterfaceService.currentDeviceAddressFlow.value.takeUnless(::isUnsupportedAddress), + ) + } + + private fun isUnsupportedAddress(address: String?): Boolean = (!bluetoothSupported && isBleAddress(address)) || + (!usbSupported && address?.firstOrNull() == InterfaceId.SERIAL.id) /** The persisted device name from the last selection, for use as a UI fallback. */ val persistedDeviceName: StateFlow = radioPrefs.devName @@ -364,25 +396,34 @@ open class ScannerViewModel( /** The single transport pane currently rendered by the Connections screen. */ val activeTransport: StateFlow = - combine(uiPrefs.selectedConnectionTransport, selectedAddressFlow) { preferred, selectedAddress -> - resolveActiveTransport(preferred, selectedAddress) - } + // The unmasked address, so a restored serial address still resolves to Network rather than the BLE default. + combine( + uiPrefs.selectedConnectionTransport, + radioInterfaceService.currentDeviceAddressFlow, + ::resolveActiveTransport, + ) .distinctUntilChanged() .stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = - resolveActiveTransport(uiPrefs.selectedConnectionTransport.value, selectedAddressFlow.value), + resolveActiveTransport( + uiPrefs.selectedConnectionTransport.value, + radioInterfaceService.currentDeviceAddressFlow.value, + ), ) /** Selects one Connections transport pane and stops scans that cannot belong to that pane. */ fun selectTransport(type: DeviceType) { + if (type == DeviceType.BLE && !bluetoothSupported) return + if (type == DeviceType.USB && !usbSupported) return when (type) { DeviceType.BLE -> stopNetworkScan() DeviceType.TCP -> stopBleScan() DeviceType.USB -> stopAllScans() } - if (activeTransport.value != type) uiPrefs.setSelectedConnectionTransport(type) + // Compared with the preference, not the pane: a fallback pane is shown without being persisted. + if (uiPrefs.selectedConnectionTransport.value != type) uiPrefs.setSelectedConnectionTransport(type) } // ── Scan commands ──────────────────────────────────────────────────────────────────────── @@ -395,7 +436,9 @@ open class ScannerViewModel( * prior scan cannot reset the flag on this new scan's state. */ fun startBleScan() { - if (_isBleScanning.value || bleScanner == null || scanStartFailureCooldownActive.value) return + if (_isBleScanning.value || bleScanner == null || !bluetoothSupported || scanStartFailureCooldownActive.value) { + return + } // Cancel the other scan first so only one flag is ever true. Both stop methods are idempotent. stopNetworkScan() @@ -759,8 +802,15 @@ open class ScannerViewModel( } } - private fun resolveActiveTransport(preferred: DeviceType?, selectedAddress: String?): DeviceType = - preferred ?: selectedAddress?.let(DeviceType::fromAddress) ?: DeviceType.BLE + private fun resolveActiveTransport(preferred: DeviceType?, selectedAddress: String?): DeviceType { + // A persisted pane or a restored address can still name a transport this hardware lacks; Network always works. + val resolved = preferred ?: selectedAddress?.let(DeviceType::fromAddress) ?: DeviceType.BLE + return when { + resolved == DeviceType.BLE && !bluetoothSupported -> DeviceType.TCP + resolved == DeviceType.USB && !usbSupported -> DeviceType.TCP + else -> resolved + } + } private fun recordSelectedTransport(fullAddress: String) { DeviceType.fromAddress(fullAddress)?.let(uiPrefs::setSelectedConnectionTransport) diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt index d0938354fc..59b9603ea0 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt @@ -34,8 +34,6 @@ import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType import org.meshtastic.core.model.Node -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Search import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.viewmodel.ConnectionStatus import org.meshtastic.feature.connections.model.DeviceListEntry @@ -45,7 +43,6 @@ import org.meshtastic.feature.connections.ui.components.DeviceList import org.meshtastic.feature.connections.ui.components.DeviceListItem import org.meshtastic.feature.connections.ui.components.DeviceSectionHeader import org.meshtastic.feature.connections.ui.components.DisconnectButton -import org.meshtastic.feature.connections.ui.components.EmptyStateContent import org.meshtastic.feature.connections.ui.components.TransportSelector import org.meshtastic.proto.User @@ -98,18 +95,6 @@ fun ConnectingDeviceInfoPreview() { } } -@PreviewLightDark -@Composable -fun EmptyStateContentPreview() { - // Bounded height so the docs reference is a tight crop of the empty-state block, not a full-screen frame - // (EmptyStateContent fills its parent to center its content). - AppTheme { - Surface(modifier = Modifier.fillMaxWidth().height(220.dp)) { - EmptyStateContent(text = "No devices found", imageVector = MeshtasticIcons.Search) - } - } -} - // Real Connections-screen Bluetooth scan: the device list with the scan-in-progress header and a discovered radio. // Replaces the old wifi-provision "Searching for device…" splash that was mislabeled as the BLE scan in the docs. @PreviewLightDark @@ -160,6 +145,30 @@ fun TransportSelectorPreview() { } } +@PreviewLightDark +@Composable +fun TransportSelectorNoBluetoothPreview() { + AppTheme { + Surface { + Box(modifier = Modifier.width(360.dp).padding(16.dp)) { + TransportSelector(activeTransport = DeviceType.TCP, onSelectTransport = {}, showBluetooth = false) + } + } + } +} + +@PreviewLightDark +@Composable +fun TransportSelectorNoUsbPreview() { + AppTheme { + Surface { + Box(modifier = Modifier.width(360.dp).padding(16.dp)) { + TransportSelector(activeTransport = DeviceType.BLE, onSelectTransport = {}, showUsb = false) + } + } + } +} + @PreviewLightDark @Composable private fun BluetoothPanePreview() { @@ -261,6 +270,30 @@ private fun UsbPaneEmptyPreview() { } } +@PreviewLightDark +@Composable +fun DemoModeSectionPreview() { + AppTheme { + DeviceList( + connectionState = ConnectionState.Disconnected, + selectedDevice = "", + bleDevices = emptyList(), + usbDevices = emptyList(), + discoveredTcpDevices = emptyList(), + recentTcpDevices = emptyList(), + isBleScanning = false, + isNetworkScanning = false, + activeTransport = DeviceType.TCP, + onSelectDevice = {}, + onToggleBleScan = {}, + onToggleNetworkScan = {}, + onAddManualAddress = { _, _ -> }, + onRemoveRecentAddress = {}, + virtualDevices = listOf(DeviceListEntry.Mock("Demo Mode"), DeviceListEntry.Replay("Demo Mode (Replay)")), + ) + } +} + private class PreviewBleDevice( override val address: String, override val name: String?, diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt index 9069e37f9f..fed424e169 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt @@ -66,12 +66,11 @@ open class CommonGetDiscoveredDevicesUseCase( val discoveredTcpForUi = matchDiscoveredTcpNodes(processedTcp, db, resolved, databaseManager) val recentTcpForUi = buildRecentTcpEntries(recentList, discoveredTcpAddresses, db, databaseManager) - val mockEntries = virtualDeviceEntries(showMock, showReplay) - DiscoveredDevices( discoveredTcpDevices = discoveredTcpForUi, recentTcpDevices = recentTcpForUi, - usbDevices = usbList + mockEntries, + usbDevices = usbList, + virtualDevices = virtualDeviceEntries(showMock, showReplay), ) } } diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/model/DiscoveredDevices.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/model/DiscoveredDevices.kt index 1ca4621f6c..d24e876b63 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/model/DiscoveredDevices.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/model/DiscoveredDevices.kt @@ -24,6 +24,8 @@ data class DiscoveredDevices( val usbDevices: List = emptyList(), val discoveredTcpDevices: List = emptyList(), val recentTcpDevices: List = emptyList(), + /** Demo Mode entries, offered under every transport pane because they belong to none of them. */ + val virtualDevices: List = emptyList(), ) interface GetDiscoveredDevicesUseCase { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt index 9aba8061cb..9d8802398e 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt @@ -16,15 +16,20 @@ */ package org.meshtastic.feature.connections.navigation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel +import org.meshtastic.core.common.state.LaunchOptions import org.meshtastic.core.navigation.ConnectionsRoute import org.meshtastic.core.navigation.NodesRoute import org.meshtastic.core.resources.Res @@ -47,27 +52,18 @@ fun EntryProviderScope.connectionsGraph(backStack: NavBackStack) // A deep link (e.g. from AI/automation tooling) may name a device address, or `n` to disconnect. The // `connections` path is a verified https://meshtastic.org app link, so any web page can fire one at us — - // always confirm before re-pointing or dropping the radio connection. + // confirm before re-pointing or dropping the radio connection. Only a debug launch switch skips it. var pendingAddress by rememberSaveable(key.address) { mutableStateOf(key.address?.takeIf(String::isNotBlank)) } + val launchOptions = koinInject() pendingAddress?.let { address -> - val isDisconnect = address == NO_DEVICE_SELECTED - MeshtasticDialog( - titleRes = - if (isDisconnect) Res.string.deep_link_disconnect_title else Res.string.deep_link_connect_title, - message = - if (isDisconnect) { - stringResource(Res.string.deep_link_disconnect_message) - } else { - stringResource(Res.string.deep_link_connect_message, address) + DeepLinkConnectPrompt( + address = address, + skipConfirmation = launchOptions.skipDeepLinkConfirmation, + onApply = { + if (it == NO_DEVICE_SELECTED) scanModel.disconnect() else scanModel.changeDeviceAddress(it) }, - confirmTextRes = if (isDisconnect) Res.string.disconnect else Res.string.connect, - onConfirm = { - if (isDisconnect) scanModel.disconnect() else scanModel.changeDeviceAddress(address) - pendingAddress = null - }, - dismissTextRes = Res.string.cancel, - onDismiss = { pendingAddress = null }, + onDone = { pendingAddress = null }, ) } @@ -79,3 +75,42 @@ fun EntryProviderScope.connectionsGraph(backStack: NavBackStack) ) } } + +/** + * The trust step for a `connections` deep link. The address is applied only once the user confirms, unless + * [skipConfirmation], a debug launch switch, says to apply it straight away. + */ +@Composable +internal fun DeepLinkConnectPrompt( + address: String, + skipConfirmation: Boolean, + onApply: (String) -> Unit, + onDone: () -> Unit, +) { + if (skipConfirmation) { + val apply by rememberUpdatedState(onApply) + val done by rememberUpdatedState(onDone) + LaunchedEffect(address) { + apply(address) + done() + } + } else { + val isDisconnect = address == NO_DEVICE_SELECTED + MeshtasticDialog( + titleRes = if (isDisconnect) Res.string.deep_link_disconnect_title else Res.string.deep_link_connect_title, + message = + if (isDisconnect) { + stringResource(Res.string.deep_link_disconnect_message) + } else { + stringResource(Res.string.deep_link_connect_message, address) + }, + confirmTextRes = if (isDisconnect) Res.string.disconnect else Res.string.connect, + onConfirm = { + onApply(address) + onDone() + }, + dismissTextRes = Res.string.cancel, + onDismiss = onDone, + ) + } +} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt index 8f60ddee24..dcba049e2c 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt @@ -243,6 +243,7 @@ fun ConnectionsScreen( val isBleScanning by scanModel.isBleScanning.collectAsStateWithLifecycle() val isNetworkScanning by scanModel.isNetworkScanning.collectAsStateWithLifecycle() val activeTransport by scanModel.activeTransport.collectAsStateWithLifecycle() + val virtualDevices by scanModel.virtualDevicesForUi.collectAsStateWithLifecycle() val blePermissionRefusal by scanModel.blePermissionRefusal.collectAsStateWithLifecycle() val bleAutoScan by scanModel.bleAutoScan.collectAsStateWithLifecycle() val networkAutoScan by scanModel.networkAutoScan.collectAsStateWithLifecycle() @@ -487,6 +488,7 @@ fun ConnectionsScreen( discoveredTcpDevices = discoveredTcpDevices, recentTcpDevices = recentTcpDevices, usbDevices = usbDevices, + virtualDevices = virtualDevices, connectionStatus = connectionStatus, connectionProgress = connectionProgress, onClickDisconnect = { scanModel.disconnect() }, @@ -530,7 +532,7 @@ fun ConnectionsScreen( // path (it uses the live connection when connected); cleared automatically once the device // returns on its own. pendingRecovery - ?.takeIf { connectionState !is ConnectionState.Connected } + ?.takeIf { connectionState !is ConnectionState.Connected && scanModel.bluetoothSupported } ?.let { recovery -> Spacer(modifier = Modifier.height(8.dp)) RecoveryCard( @@ -592,10 +594,15 @@ fun ConnectionsScreen( // Transport selector sits between the connection card and device list; it controls only the // visible discovery pane, not the globally selected/connected device shown above. - TransportSelector( - activeTransport = activeTransport, - onSelectTransport = scanModel::selectTransport, - ) + // With Network as the only pane left, a one-segment control would select nothing. + if (scanModel.bluetoothSupported || scanModel.usbSupported) { + TransportSelector( + activeTransport = activeTransport, + onSelectTransport = scanModel::selectTransport, + showBluetooth = scanModel.bluetoothSupported, + showUsb = scanModel.usbSupported, + ) + } // Adapter-off hints: shown only when the relevant permission is granted but the radio/network // is unavailable, so they don't overlap the permission-recovery flow on the scan toggles. @@ -699,6 +706,7 @@ fun ConnectionsScreen( selectedDevice = selectedDevice, bleDevices = bleDevices, usbDevices = usbDevices, + virtualDevices = virtualDevices, discoveredTcpDevices = discoveredTcpDevices, recentTcpDevices = recentTcpDevices, isBleScanning = isBleScanning, @@ -876,6 +884,7 @@ private fun ConnectingDeviceContent( discoveredTcpDevices: List, recentTcpDevices: List, usbDevices: List, + virtualDevices: List, connectionStatus: ConnectionStatus, connectionProgress: String?, onClickDisconnect: () -> Unit, @@ -885,6 +894,7 @@ private fun ConnectingDeviceContent( ?: discoveredTcpDevices.find { it.fullAddress == selectedDevice } ?: recentTcpDevices.find { it.fullAddress == selectedDevice } ?: usbDevices.find { it.fullAddress == selectedDevice } + ?: virtualDevices.find { it.fullAddress == selectedDevice } // Use the entry name if found in scan lists, otherwise fall back to the persisted name // from the last successful selection, and only show "Unknown Device" as a last resort. diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt index 76a3385f3d..7c85841651 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt @@ -146,7 +146,7 @@ fun CurrentlyConnectedInfo( } } -@Suppress("MagicNumber", "UnusedPrivateMember") +@Suppress("MagicNumber", "UnusedPrivateFunction") @Composable private fun CurrentlyConnectedInfoPreview() { AppTheme { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceList.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceList.kt index b6b1ea5daf..5f1d74f554 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceList.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceList.kt @@ -69,6 +69,7 @@ import org.meshtastic.core.resources.add_network_device_manually import org.meshtastic.core.resources.address import org.meshtastic.core.resources.bluetooth import org.meshtastic.core.resources.cancel +import org.meshtastic.core.resources.demo_mode import org.meshtastic.core.resources.ip_port import org.meshtastic.core.resources.network import org.meshtastic.core.resources.no_bluetooth_devices_hint @@ -120,6 +121,7 @@ fun DeviceList( onAddManualAddress: (address: String, fullAddress: String) -> Unit, onRemoveRecentAddress: (DeviceListEntry) -> Unit, modifier: Modifier = Modifier, + virtualDevices: List = emptyList(), ) { var showAddDialog by remember { mutableStateOf(false) } val sheetState = @@ -177,6 +179,33 @@ fun DeviceList( onSelectDevice = onSelectDevice, ) } + if (virtualDevices.isNotEmpty()) { + virtualSection( + virtualDevices = virtualDevices, + connectionState = connectionState, + selectedDevice = selectedDevice, + onSelectDevice = onSelectDevice, + ) + } + } +} + +private fun LazyListScope.virtualSection( + virtualDevices: List, + connectionState: ConnectionState, + selectedDevice: String, + onSelectDevice: (DeviceListEntry) -> Unit, +) { + item(key = "header:demo", contentType = "header") { + DeviceSectionHeader(title = stringResource(Res.string.demo_mode)) + } + items(virtualDevices, key = { device -> "demo:${device.fullAddress}" }, contentType = { "device" }) { device -> + DeviceCard( + device = device, + connectionState = connectionState, + selectedDevice = selectedDevice, + onSelect = onSelectDevice, + ) } } diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/TransportSelector.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/TransportSelector.kt index dd7b2310df..73527545c6 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/TransportSelector.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/TransportSelector.kt @@ -40,13 +40,14 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Usb import org.meshtastic.core.ui.icon.Wifi -private const val TRANSPORT_COUNT = 3 - /** * Single-choice transport selector rendered below the connection card. A Material 3 [SingleChoiceSegmentedButtonRow] * makes the mutually-exclusive choice explicit: the segments read as one grouped control and the selected transport * shows a check, rather than three independent chips whose filled state was read as "enabled/available" instead of * "selected". + * + * @param showBluetooth false on hardware with no Bluetooth LE, where the BLE segment could never find anything. + * @param showUsb false on hardware with no USB host, unless Demo Mode needs the USB pane. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -54,34 +55,44 @@ fun TransportSelector( activeTransport: DeviceType, onSelectTransport: (DeviceType) -> Unit, modifier: Modifier = Modifier, + showBluetooth: Boolean = true, + showUsb: Boolean = true, ) { + val transports = + DeviceType.entries.filter { (it != DeviceType.BLE || showBluetooth) && (it != DeviceType.USB || showUsb) } // Fill the width so the control reads as one deliberate group spanning the same width as the connection card - // above; each SegmentedButton carries an internal weight(1f), so the three segments divide the row evenly. + // above; each SegmentedButton carries an internal weight(1f), so the segments divide the row evenly. SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { - TransportSegment( - selected = activeTransport == DeviceType.BLE, - index = 0, - label = Res.string.bluetooth, - icon = MeshtasticIcons.Bluetooth, - onClick = { onSelectTransport(DeviceType.BLE) }, - ) - TransportSegment( - selected = activeTransport == DeviceType.TCP, - index = 1, - label = Res.string.network, - icon = MeshtasticIcons.Wifi, - onClick = { onSelectTransport(DeviceType.TCP) }, - ) - TransportSegment( - selected = activeTransport == DeviceType.USB, - index = 2, - label = Res.string.usb, - icon = MeshtasticIcons.Usb, - onClick = { onSelectTransport(DeviceType.USB) }, - ) + transports.forEachIndexed { index, transport -> + TransportSegment( + selected = activeTransport == transport, + index = index, + count = transports.size, + label = transport.label, + icon = transport.icon, + onClick = { onSelectTransport(transport) }, + ) + } } } +private val DeviceType.label: StringResource + get() = + when (this) { + DeviceType.BLE -> Res.string.bluetooth + DeviceType.TCP -> Res.string.network + DeviceType.USB -> Res.string.usb + } + +private val DeviceType.icon: ImageVector + @Composable + get() = + when (this) { + DeviceType.BLE -> MeshtasticIcons.Bluetooth + DeviceType.TCP -> MeshtasticIcons.Wifi + DeviceType.USB -> MeshtasticIcons.Usb + } + /** * A single transport segment: shows a check when [selected] and the transport [icon] otherwise, so selection is * unambiguous while the unselected segments still communicate which transport they represent. @@ -91,6 +102,7 @@ fun TransportSelector( private fun SingleChoiceSegmentedButtonRowScope.TransportSegment( selected: Boolean, index: Int, + count: Int, label: StringResource, icon: ImageVector, onClick: () -> Unit, @@ -98,7 +110,7 @@ private fun SingleChoiceSegmentedButtonRowScope.TransportSegment( SegmentedButton( selected = selected, onClick = onClick, - shape = SegmentedButtonDefaults.itemShape(index = index, count = TRANSPORT_COUNT), + shape = SegmentedButtonDefaults.itemShape(index = index, count = count), icon = { SegmentedButtonDefaults.Icon(active = selected) { Icon( diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt index 55351bdb36..0bb5ad1545 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt +++ b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt @@ -143,19 +143,22 @@ class ScannerViewModelHarness(val testDispatcher: TestDispatcher = UnconfinedTes * Build the platform-neutral [ScannerViewModel]. Call only after `Dispatchers.setMain(testDispatcher)` because the * ViewModel's `init` launches work on `viewModelScope` (Main). */ - fun buildBase(): ScannerViewModel = ScannerViewModel( - serviceRepository = serviceRepository, - radioController = radioController, - radioInterfaceService = radioInterfaceService, - radioPrefs = radioPrefs, - recentAddressesDataSource = recentAddressesDataSource, - getDiscoveredDevicesUseCase = getDiscoveredDevicesUseCase, - networkRepository = networkRepository, - dispatchers = dispatchers, - uiPrefs = uiPrefs, - firmwareRecoveryDataSource = firmwareRecoveryDataSource, - bleScanner = bleScanner, - ) + fun buildBase(bluetoothSupported: Boolean = true, usbSupported: Boolean = true): ScannerViewModel = + ScannerViewModel( + serviceRepository = serviceRepository, + radioController = radioController, + radioInterfaceService = radioInterfaceService, + radioPrefs = radioPrefs, + recentAddressesDataSource = recentAddressesDataSource, + getDiscoveredDevicesUseCase = getDiscoveredDevicesUseCase, + networkRepository = networkRepository, + dispatchers = dispatchers, + uiPrefs = uiPrefs, + firmwareRecoveryDataSource = firmwareRecoveryDataSource, + bleScanner = bleScanner, + bluetoothSupported = bluetoothSupported, + usbSupported = usbSupported, + ) /** * Ends [viewModel]'s lifetime. Call from `@AfterTest` **before** `Dispatchers.resetMain()`. diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt index 843c3ff814..951109c2af 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt +++ b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt @@ -20,6 +20,8 @@ import app.cash.turbine.test import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.matcher.any +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode import dev.mokkery.verifySuspend import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -420,6 +422,177 @@ class ScannerViewModelTest { assertEquals(DeviceType.BLE, viewModel.activeTransport.value) } + @Test + fun `active transport falls back to Network on hardware without Bluetooth`() { + harness.uiPrefs.setSelectedConnectionTransport(DeviceType.BLE) + val noBluetooth = harness.buildBase(bluetoothSupported = false) + try { + assertEquals(DeviceType.TCP, noBluetooth.activeTransport.value) + + noBluetooth.selectTransport(DeviceType.BLE) + assertEquals(DeviceType.TCP, noBluetooth.activeTransport.value) + } finally { + harness.clearViewModel(noBluetooth) + } + } + + @Test + fun `a restored BLE address does not select the BLE pane on hardware without Bluetooth`() { + harness.currentDeviceAddressFlow.value = "xAA:BB:CC:DD:EE:FF" + val noBluetooth = harness.buildBase(bluetoothSupported = false) + try { + assertEquals(DeviceType.TCP, noBluetooth.activeTransport.value) + } finally { + harness.clearViewModel(noBluetooth) + } + } + + @Test + fun `a restored BLE address reads as no device on hardware without Bluetooth`() = runTest { + harness.currentDeviceAddressFlow.value = "xAA:BB:CC:DD:EE:FF" + val noBluetooth = harness.buildBase(bluetoothSupported = false) + try { + assertEquals(null, noBluetooth.selectedAddressFlow.value) + noBluetooth.selectedNotNullFlow.test { assertEquals(NO_DEVICE_SELECTED, awaitItem()) } + + harness.currentDeviceAddressFlow.value = "t10.0.0.2" + assertEquals("t10.0.0.2", noBluetooth.selectedAddressFlow.value) + } finally { + harness.clearViewModel(noBluetooth) + } + } + + @Test + fun `a BLE address stays selected when Bluetooth is present`() = runTest { + harness.currentDeviceAddressFlow.value = "xAA:BB:CC:DD:EE:FF" + val withBluetooth = harness.buildBase() + try { + withBluetooth.selectedNotNullFlow.test { assertEquals("xAA:BB:CC:DD:EE:FF", awaitItem()) } + } finally { + harness.clearViewModel(withBluetooth) + } + } + + @Test + fun `active transport falls back to Network on hardware without USB host`() { + harness.uiPrefs.setSelectedConnectionTransport(DeviceType.USB) + val noUsb = harness.buildBase(usbSupported = false) + try { + assertEquals(DeviceType.TCP, noUsb.activeTransport.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `a restored serial address does not select the USB pane on hardware without USB host`() { + harness.currentDeviceAddressFlow.value = "s/dev/bus/usb/001/002" + val noUsb = harness.buildBase(usbSupported = false) + try { + assertEquals(DeviceType.TCP, noUsb.activeTransport.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `a restored serial address reads as no device and lets Network auto-scan run without USB host`() { + harness.currentDeviceAddressFlow.value = "s/dev/bus/usb/001/002" + val noUsb = harness.buildBase(usbSupported = false) + try { + assertEquals(null, noUsb.selectedAddressFlow.value) + + noUsb.startNetworkAutoScan() + assertEquals(true, noUsb.isNetworkScanning.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `the Demo Mode address stays selected without USB host`() { + harness.currentDeviceAddressFlow.value = "m" + val noUsb = harness.buildBase(usbSupported = false) + try { + assertEquals("m", noUsb.selectedAddressFlow.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `selectTransport ignores USB on hardware without USB host`() { + val noUsb = harness.buildBase(usbSupported = false) + try { + noUsb.selectTransport(DeviceType.TCP) + noUsb.selectTransport(DeviceType.USB) + + assertEquals(DeviceType.TCP, noUsb.activeTransport.value) + assertEquals(DeviceType.TCP, harness.uiPrefs.selectedConnectionTransport.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `choosing Network over a USB fallback persists it`() { + harness.uiPrefs.setSelectedConnectionTransport(DeviceType.USB) + val noUsb = harness.buildBase(usbSupported = false) + try { + noUsb.selectTransport(DeviceType.TCP) + + assertEquals(DeviceType.TCP, harness.uiPrefs.selectedConnectionTransport.value) + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `Demo Mode entries are listed apart from USB devices without USB host`() = runTest { + harness.mockTransportEnabled.value = true + baseDevicesFlow.value = DiscoveredDevices(virtualDevices = listOf(DeviceListEntry.Mock("Demo Mode"))) + val noUsb = harness.buildBase(usbSupported = false) + try { + noUsb.virtualDevicesForUi.test { + assertEquals(listOf(DeviceListEntry.Mock("Demo Mode")), expectMostRecentItem()) + cancelAndIgnoreRemainingEvents() + } + noUsb.usbDevicesForUi.test { + assertEquals(emptyList(), expectMostRecentItem()) + cancelAndIgnoreRemainingEvents() + } + } finally { + harness.clearViewModel(noUsb) + } + } + + @Test + fun `a selected Demo Mode address does not choose a transport pane`() { + harness.uiPrefs.setSelectedConnectionTransport(DeviceType.TCP) + val subject = harness.buildBase() + try { + subject.onSelected(DeviceListEntry.Mock("Demo Mode")) + + assertEquals(DeviceType.TCP, subject.activeTransport.value) + assertEquals(DeviceType.TCP, harness.uiPrefs.selectedConnectionTransport.value) + } finally { + harness.clearViewModel(subject) + } + } + + @Test + fun `startBleScan never scans on hardware without Bluetooth`() { + val noBluetooth = harness.buildBase(bluetoothSupported = false) + try { + noBluetooth.startBleScan() + + assertEquals(false, noBluetooth.isBleScanning.value) + verify(mode = VerifyMode.not) { bleScanner.scan(any(), any()) } + } finally { + harness.clearViewModel(noBluetooth) + } + } + @Test fun `startNetworkScan updates isNetworkScanning`() = runTest { viewModel.isNetworkScanning.test { diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt index 0efe552c3c..19fe41f3d1 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt +++ b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt @@ -91,7 +91,8 @@ class CommonGetDiscoveredDevicesUseCaseTest { setUp() useCase.invoke(showMock = true, showReplay = false, resolvedList = resolvedServicesFlow).test { val result = awaitItem() - result.usbDevices.map { it::class } shouldBe listOf(DeviceListEntry.Mock::class) + result.virtualDevices.map { it::class } shouldBe listOf(DeviceListEntry.Mock::class) + assertTrue(result.usbDevices.isEmpty(), "Demo Mode is not a USB device") cancelAndIgnoreRemainingEvents() } } @@ -101,7 +102,7 @@ class CommonGetDiscoveredDevicesUseCaseTest { setUp() useCase.invoke(showMock = true, showReplay = true, resolvedList = resolvedServicesFlow).test { val result = awaitItem() - result.usbDevices.map { it::class } shouldBe + result.virtualDevices.map { it::class } shouldBe listOf(DeviceListEntry.Mock::class, DeviceListEntry.Replay::class) cancelAndIgnoreRemainingEvents() } @@ -113,7 +114,7 @@ class CommonGetDiscoveredDevicesUseCaseTest { setUp() useCase.invoke(showMock = false, showReplay = true, resolvedList = resolvedServicesFlow).test { val result = awaitItem() - assertTrue(result.usbDevices.isEmpty(), "No replay device when showMock=false") + assertTrue(result.virtualDevices.isEmpty(), "No replay device when showMock=false") cancelAndIgnoreRemainingEvents() } } @@ -123,7 +124,7 @@ class CommonGetDiscoveredDevicesUseCaseTest { setUp() useCase.invoke(showMock = false, showReplay = false, resolvedList = resolvedServicesFlow).test { val result = awaitItem() - assertTrue(result.usbDevices.isEmpty(), "No mock device when showMock=false") + assertTrue(result.virtualDevices.isEmpty(), "No mock device when showMock=false") cancelAndIgnoreRemainingEvents() } } @@ -262,7 +263,7 @@ class CommonGetDiscoveredDevicesUseCaseTest { setUp() useCase.invoke(showMock = true, showReplay = true, resolvedList = flowOf(emptyList())).test { val result = awaitItem() - result.usbDevices.map { it::class } shouldBe + result.virtualDevices.map { it::class } shouldBe listOf(DeviceListEntry.Mock::class, DeviceListEntry.Replay::class) cancelAndIgnoreRemainingEvents() } diff --git a/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/navigation/DeepLinkConnectPromptTest.kt b/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/navigation/DeepLinkConnectPromptTest.kt new file mode 100644 index 0000000000..70e91ff73f --- /dev/null +++ b/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/navigation/DeepLinkConnectPromptTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.connections.navigation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.cancel +import org.meshtastic.core.resources.connect +import org.meshtastic.core.resources.deep_link_connect_title +import org.meshtastic.core.resources.getString +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalTestApi::class) +class DeepLinkConnectPromptTest { + + private val applied = mutableListOf() + private var done = 0 + + @Test + fun `a deep link address waits for the user to confirm`() = runComposeUiTest { + setPrompt(skipConfirmation = false) + + onNodeWithText(getString(Res.string.deep_link_connect_title)).assertIsDisplayed() + assertTrue(applied.isEmpty()) + + onNodeWithText(getString(Res.string.connect)).performClick() + waitForIdle() + + assertEquals(listOf(ADDRESS), applied) + assertEquals(1, done) + } + + @Test + fun `cancelling the prompt applies nothing`() = runComposeUiTest { + setPrompt(skipConfirmation = false) + + onNodeWithText(getString(Res.string.cancel)).performClick() + waitForIdle() + + assertTrue(applied.isEmpty()) + assertEquals(1, done) + } + + @Test + fun `the launch switch applies the address once with no dialog`() = runComposeUiTest { + setPrompt(skipConfirmation = true) + waitForIdle() + + onNodeWithText(getString(Res.string.deep_link_connect_title)).assertDoesNotExist() + assertEquals(listOf(ADDRESS), applied) + assertEquals(1, done) + } + + private fun ComposeUiTest.setPrompt(skipConfirmation: Boolean) { + setContent { + MaterialTheme { + DeepLinkConnectPrompt( + address = ADDRESS, + skipConfirmation = skipConfirmation, + onApply = { applied += it }, + onDone = { done++ }, + ) + } + } + } + + private companion object { + const val ADDRESS = "t10.0.2.2:4403" + } +} diff --git a/feature/discovery/README.md b/feature/discovery/README.md index e41c55f0d4..187fdcc2e6 100644 --- a/feature/discovery/README.md +++ b/feature/discovery/README.md @@ -2,7 +2,7 @@ ## Overview -The `:feature:discovery` module implements **Local Mesh Discovery**: the app cycles the connected radio through a queue of LoRa modem presets, dwells on each for a configured duration while collecting packets, then persists and ranks the results so the user can see which preset (or beacon-advertised custom channel) has the most active mesh nearby. Sessions are stored via `DiscoveryDao` and can be revisited, mapped, summarised (AI or algorithmic), and exported. +The `:feature:discovery` module implements **Local Mesh Discovery**: the app cycles the connected radio through a queue of LoRa modem presets, dwells on each for a configured duration while collecting packets, then persists and ranks the results so the user can see which preset (or beacon-advertised custom channel) has the most active mesh nearby. `DiscoveryScanEngine`, `DiscoveryHomeRestorer`, `DiscoveryTerminalCoordinator` and `DiscoveryInterruptedSessionRecovery` write sessions through `DiscoveryDao` directly; the ViewModels read and edit them through `DiscoveryRepository` (`core:repository`), so they can be revisited, mapped, summarised (AI or algorithmic), and exported. **Targets:** Android · JVM (Desktop) · iOS (via `meshtastic.kmp.feature` convention plugin) @@ -40,7 +40,7 @@ Multiplatform export path in `export/`: - `DiscoveryExporter` — interface: `export(DiscoveryExportData): ExportResult` (success = bytes + MIME type + filename). - `DiscoveryReportFormatter` — shared formatting of session/preset report lines and filenames. -- `rememberExportSaver()` / `ExportSaverLauncher` — `expect`/`actual` file-save seam per platform (`ExportSaver.android.kt` → SAF document picker, `ExportSaver.jvm.kt` → file dialog, `ExportSaver.ios.kt`). +- `rememberExportSaver()` / `ExportSaverLauncher` save an `ExportResult.Success` to a user-picked file through `core:ui`'s `rememberFileExporter`. - `PdfDiscoveryExporter` (androidMain) renders a PDF report; `TextDiscoveryExporter` (jvmMain) renders plain text. ## Mesh Beacon Invitations @@ -55,5 +55,4 @@ UI for port-37 Mesh Beacon join invitations (`ui/component/`): From `feature/discovery/build.gradle.kts` (`commonMain`): - `core:common`, `core:data`, `core:database`, `core:di`, `core:model`, `core:navigation`, `core:network`, `core:prefs`, `core:repository`, `core:resources`, `core:service`, `core:ui` -- `kotlinx.collections.immutable` - `org.meshtastic:protobufs` (Maven artifact) diff --git a/feature/discovery/build.gradle.kts b/feature/discovery/build.gradle.kts index 8cdc85d3da..d4fa3c8e27 100644 --- a/feature/discovery/build.gradle.kts +++ b/feature/discovery/build.gradle.kts @@ -33,20 +33,17 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.navigation) - implementation(projects.core.network) - implementation(projects.core.prefs) implementation(projects.core.repository) implementation(projects.core.resources) - implementation(projects.core.service) implementation(projects.core.ui) - implementation(libs.kotlinx.collections.immutable) implementation(libs.meshtastic.protobufs) } + + commonTest.dependencies { implementation(projects.core.data) } } } diff --git a/feature/discovery/detekt-baseline.xml b/feature/discovery/detekt-baseline.xml new file mode 100644 index 0000000000..eeb8025365 --- /dev/null +++ b/feature/discovery/detekt-baseline.xml @@ -0,0 +1,11 @@ + + + + + NoNameShadowing:DiscoveryScanEngine.kt:DiscoveryScanEngine$wb + UnnecessaryLaunchedEffect:DiscoveryScanScreen.kt:LaunchedEffect + UnnecessaryLaunchedEffect:DiscoverySummaryScreen.kt:LaunchedEffect + UnusedPrivateProperty:DiscoveryScanEngine.kt:DiscoveryScanEngine$private val applicationScope: ApplicationCoroutineScope + UnusedPrivateProperty:DiscoveryViewModel.kt:DiscoveryViewModel$private val serviceRepository: ServiceRepository + + diff --git a/feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.android.kt b/feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.android.kt deleted file mode 100644 index e8c8e53a4d..0000000000 --- a/feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.android.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.discovery.export - -import android.content.Intent -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@Composable -actual fun rememberExportSaver(): ExportSaverLauncher { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val pendingExport = remember { mutableStateOf(null) } - - val launcher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - val export = pendingExport.value ?: return@rememberLauncherForActivityResult - pendingExport.value = null - scope.launch { - withContext(Dispatchers.IO) { - @Suppress("TooGenericExceptionCaught") - try { - context.contentResolver.openOutputStream(uri)?.use { it.write(export.content) } - } catch (e: Exception) { - Logger.e(throwable = e) { "Failed to write export file" } - } - } - } - } - - return ExportSaverLauncher { result -> - pendingExport.value = result - val intent = - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = result.mimeType - putExtra(Intent.EXTRA_TITLE, result.fileName) - } - launcher.launch(intent) - } -} diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryDetailViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryDetailViewModel.kt index 2270e880c4..49bee1ddbd 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryDetailViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryDetailViewModel.kt @@ -22,24 +22,24 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.koin.core.annotation.InjectedParam import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity +import org.meshtastic.core.repository.DiscoveryRepository import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed @KoinViewModel class DiscoveryHistoryDetailViewModel( @InjectedParam private val sessionId: Long, - private val discoveryDao: DiscoveryDao, + private val discoveryRepository: DiscoveryRepository, ) : ViewModel() { val session: StateFlow = - discoveryDao.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) + discoveryRepository.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) val presetResults: StateFlow> = - discoveryDao.getPresetResultsFlow(sessionId).stateInWhileSubscribed(initialValue = emptyList()) + discoveryRepository.getPresetResultsFlow(sessionId).stateInWhileSubscribed(initialValue = emptyList()) private val _nodesByPreset = MutableStateFlow>>(emptyMap()) val nodesByPreset: StateFlow>> = _nodesByPreset.asStateFlow() @@ -50,12 +50,8 @@ class DiscoveryHistoryDetailViewModel( private fun loadNodes() { safeLaunch(tag = "loadNodes") { - val results = discoveryDao.getPresetResults(sessionId) - val nodesMap = mutableMapOf>() - for (result in results) { - nodesMap[result.id] = discoveryDao.getDiscoveredNodes(result.id) - } - _nodesByPreset.value = nodesMap + val results = discoveryRepository.getPresetResults(sessionId) + _nodesByPreset.value = discoveryRepository.getNodesByPresetResult(results.map { it.id }) } } } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryViewModel.kt index 40b892fb3a..d5d2ba6d76 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryViewModel.kt @@ -19,18 +19,18 @@ package org.meshtastic.feature.discovery import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoverySessionEntity +import org.meshtastic.core.repository.DiscoveryRepository import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed @KoinViewModel -class DiscoveryHistoryViewModel(private val discoveryDao: DiscoveryDao) : ViewModel() { +class DiscoveryHistoryViewModel(private val discoveryRepository: DiscoveryRepository) : ViewModel() { val sessions: StateFlow> = - discoveryDao.getAllSessions().stateInWhileSubscribed(initialValue = emptyList()) + discoveryRepository.getAllSessions().stateInWhileSubscribed(initialValue = emptyList()) fun deleteSession(sessionId: Long) { - safeLaunch(tag = "deleteSession") { discoveryDao.deleteSession(sessionId) } + safeLaunch(tag = "deleteSession") { discoveryRepository.deleteSession(sessionId) } } } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt index 451c655c77..059bc7abce 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt @@ -68,6 +68,7 @@ internal fun finalStatusForPendingRestore( else -> default } +@Suppress("SuspendFunSwallowedCancellation") // the waiter's own cancellation is rethrown by ensureActive() private suspend fun awaitRestoreResult(result: Deferred, timeout: kotlin.time.Duration): Boolean { val completed = withTimeoutOrNull(timeout) { @@ -179,29 +180,28 @@ internal class DiscoveryHomeRestorer( suspend fun schedule(plan: DiscoveryHomeRestorePlan): Deferred { var superseded: PendingRestore? = null var created = false - val pending = - pendingMutex.withLock { - val existing = pendingRestore - if ( - existing != null && - !existing.result.isCompleted && - existing.plan.sessionId == plan.sessionId && - existing.plan.deviceAddress == plan.deviceAddress - ) { - existing - } else { - superseded = existing?.takeUnless { it.result.isCompleted } - val state = RestoreState(plan.finalStatus) - val result = - applicationScope.async(start = CoroutineStart.LAZY) { - restoreUntilComplete(plan, state).also { state.completedSuccessfully.value = it } - } - PendingRestore(plan, result, state).also { - publishPendingRestore(it) - created = true + val pending = pendingMutex.withLock { + val existing = pendingRestore + if ( + existing != null && + !existing.result.isCompleted && + existing.plan.sessionId == plan.sessionId && + existing.plan.deviceAddress == plan.deviceAddress + ) { + existing + } else { + superseded = existing?.takeUnless { it.result.isCompleted } + val state = RestoreState(plan.finalStatus) + val result = + applicationScope.async(start = CoroutineStart.LAZY) { + restoreUntilComplete(plan, state).also { state.completedSuccessfully.value = it } } + PendingRestore(plan, result, state).also { + publishPendingRestore(it) + created = true } } + } superseded?.result?.cancel() if (created) { pending.result.invokeOnCompletion { cause -> @@ -222,24 +222,22 @@ internal class DiscoveryHomeRestorer( current.state.finalStatus = finalStatus current } ?: return - val correction = - persistenceMutex.withLock { - val shouldCorrect = - pendingMutex.withLock { - pendingRestore === pending && - pending.state.finalStatus == finalStatus && - pending.state.persistedFinalStatus?.let { it != finalStatus } == true - } - if (!shouldCorrect) return@withLock null - - val result = safeCatching { discoveryDao.updateSessionCompletionStatus(sessionId, finalStatus) } - if (result.getOrNull() == 1) { - pendingMutex.withLock { - if (pendingRestore === pending) pending.state.persistedFinalStatus = finalStatus - } - } - result + val correction = persistenceMutex.withLock { + val shouldCorrect = pendingMutex.withLock { + pendingRestore === pending && + pending.state.finalStatus == finalStatus && + pending.state.persistedFinalStatus?.let { it != finalStatus } == true } + if (!shouldCorrect) return@withLock null + + val result = safeCatching { discoveryDao.updateSessionCompletionStatus(sessionId, finalStatus) } + if (result.getOrNull() == 1) { + pendingMutex.withLock { + if (pendingRestore === pending) pending.state.persistedFinalStatus = finalStatus + } + } + result + } val correctionFailure = correction?.exceptionOrNull() when { correctionFailure != null -> @@ -407,17 +405,16 @@ internal class DiscoveryHomeRestorer( } private suspend fun finalizeRecoveredSessionBestEffort(sessionId: Long, state: RestoreState) { - val result = - persistenceMutex.withLock { - val finalStatus = pendingMutex.withLock { state.finalStatus } - val persistence = safeCatching { - discoveryDao.updateRecoverableSessionCompletionStatus(sessionId, finalStatus) - } - if (persistence.getOrNull() == 1) { - pendingMutex.withLock { state.persistedFinalStatus = finalStatus } - } - persistence + val result = persistenceMutex.withLock { + val finalStatus = pendingMutex.withLock { state.finalStatus } + val persistence = safeCatching { + discoveryDao.updateRecoverableSessionCompletionStatus(sessionId, finalStatus) } + if (persistence.getOrNull() == 1) { + pendingMutex.withLock { state.persistedFinalStatus = finalStatus } + } + persistence + } val failure = result.exceptionOrNull() if (failure != null) { Logger.e(failure) { @@ -441,15 +438,14 @@ internal class DiscoveryHomeRestorer( } private suspend fun markUnrestorableBestEffort(sessionId: Long): Boolean { - val result = - persistenceMutex.withLock { - safeCatching { - discoveryDao.updateRecoverableSessionCompletionStatus( - sessionId, - DiscoverySessionStatus.UNRESTORABLE, - ) - } + val result = persistenceMutex.withLock { + safeCatching { + discoveryDao.updateRecoverableSessionCompletionStatus( + sessionId, + DiscoverySessionStatus.UNRESTORABLE, + ) } + } val failure = result.exceptionOrNull() return when { failure != null -> { diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt index 92f1597171..9201771207 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt @@ -62,6 +62,7 @@ internal class DiscoveryInterruptedSessionRecovery( } } + @Suppress("SuspendFunSwallowedCancellation") // the waiter's own cancellation is rethrown by ensureActive() private suspend fun restoreIfAny(onRestored: suspend (homePreset: String) -> Unit) { val address = meshPrefs.deviceAddress.value val session = diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryMapViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryMapViewModel.kt index e2bbdcdb36..e5e737180b 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryMapViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryMapViewModel.kt @@ -23,19 +23,21 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import org.koin.core.annotation.InjectedParam import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity +import org.meshtastic.core.repository.DiscoveryRepository import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed @KoinViewModel -class DiscoveryMapViewModel(@InjectedParam private val sessionId: Long, private val discoveryDao: DiscoveryDao) : - ViewModel() { +class DiscoveryMapViewModel( + @InjectedParam private val sessionId: Long, + private val discoveryRepository: DiscoveryRepository, +) : ViewModel() { val session: StateFlow = - discoveryDao.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) + discoveryRepository.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) /** All preset results for this session. Used for filter chip UI. */ private val presetResultsState = MutableStateFlow>(emptyList()) @@ -64,8 +66,8 @@ class DiscoveryMapViewModel(@InjectedParam private val sessionId: Long, private } else { nodesByPreset[filter].orEmpty() } - // Deduplicate by nodeNum — keep the entry with strongest signal - raw.groupBy { it.nodeNum }.values.map { dupes -> dupes.maxByOrNull { it.snr } ?: dupes.first() } + // Dedup by nodeNum, keeping the strongest SNR; a sighting without one loses to any reading. + raw.groupBy { it.nodeNum }.values.map { dupes -> dupes.maxWith(compareBy(nullsFirst()) { it.snr }) } } .stateInWhileSubscribed(initialValue = emptyList()) @@ -98,13 +100,9 @@ class DiscoveryMapViewModel(@InjectedParam private val sessionId: Long, private private fun loadAllNodes() { safeLaunch(tag = "loadAllNodes") { - val results = discoveryDao.getPresetResults(sessionId) + val results = discoveryRepository.getPresetResults(sessionId) presetResultsState.value = results - val nodesMap = mutableMapOf>() - for (result in results) { - nodesMap[result.id] = discoveryDao.getDiscoveredNodes(result.id) - } - nodesByPresetState.value = nodesMap + nodesByPresetState.value = discoveryRepository.getNodesByPresetResult(results.map { it.id }) } } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt index 0702830ffe..19d28c84e4 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt @@ -51,6 +51,8 @@ import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ChannelOption import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.numChannels +import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.snrOrNull import org.meshtastic.core.repository.DiscoveryPacketCollector @@ -185,7 +187,8 @@ class DiscoveryScanEngine( var neighborType: String = "direct", var latitude: Double? = null, var longitude: Double? = null, - var snr: Float = 0f, + /** Null until a packet reports one, so an absent reading stays distinct from a valid 0 dB. */ + var snr: Float? = null, /** Null until a packet reports one, so an absent reading stays distinct from a valid 0 dBm. */ var rssi: Int? = null, var hopCount: Int = 0, @@ -369,29 +372,28 @@ class DiscoveryScanEngine( /** Stops the active scan and restores the home preset. */ suspend fun stopScan() { - val request = - mutex.withLock { - if (!isActive) { - null - } else if (_scanState.value is DiscoveryScanState.Analysis) { - Logger.i { "DiscoveryScanEngine: ignoring stop after terminal analysis has started" } - null - } else { - if (_scanState.value !is DiscoveryScanState.Cancelling) { - Logger.i { "DiscoveryScanEngine: stopping scan" } - _scanState.value = DiscoveryScanState.Cancelling - } - // Freeze the scan generation before snapshotting its restore plan. - // No later target shift may race this terminal request. - cancelScanInternal() - terminalRequestLocked( - pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_STOPPED, - outcome = DiscoveryScanState.CompletionOutcome.Cancelled, - awaitRestore = false, - generateAi = false, - ) + val request = mutex.withLock { + if (!isActive) { + null + } else if (_scanState.value is DiscoveryScanState.Analysis) { + Logger.i { "DiscoveryScanEngine: ignoring stop after terminal analysis has started" } + null + } else { + if (_scanState.value !is DiscoveryScanState.Cancelling) { + Logger.i { "DiscoveryScanEngine: stopping scan" } + _scanState.value = DiscoveryScanState.Cancelling } + // Freeze the scan generation before snapshotting its restore plan. + // No later target shift may race this terminal request. + cancelScanInternal() + terminalRequestLocked( + pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_STOPPED, + outcome = DiscoveryScanState.CompletionOutcome.Cancelled, + awaitRestore = false, + generateAi = false, + ) } + } if (request != null) { terminalCoordinator.complete(request = request, beforeFinalize = ::persistCurrentDwellResults) } @@ -427,6 +429,8 @@ class DiscoveryScanEngine( mutex.withLock { val node = collectedNodes.getOrPut(fromNum) { CollectedNodeData(nodeNum = fromNum) } + // Hearing the node itself is a direct sighting, even if NeighborInfo named it first. + node.neighborType = "direct" // Update signal info from the direct packet // Explicit presence: record a reported 0 dB/0 dBm, skip only a genuinely absent one. meshPacket.snrOrNull()?.let { node.snr = it } @@ -471,21 +475,20 @@ class DiscoveryScanEngine( for (target in targets) { if (!isActive) return - val shouldShift = - mutex.withLock { - if (!canAdvanceScanLocked()) { - false - } else { - currentPresetName = target.label - totalDwellSeconds = dwellDurationSeconds - currentDwellPersisted = false - collectedNodes.clear() - deviceMetricsLog.clear() - lastLocalStats = null - _scanState.value = DiscoveryScanState.Shifting(target.label) - true - } + val shouldShift = mutex.withLock { + if (!canAdvanceScanLocked()) { + false + } else { + currentPresetName = target.label + totalDwellSeconds = dwellDurationSeconds + currentDwellPersisted = false + collectedNodes.clear() + deviceMetricsLog.clear() + lastLocalStats = null + _scanState.value = DiscoveryScanState.Shifting(target.label) + true } + } if (!shouldShift) return // Shift to the new target (preset, plus a custom primary channel for beacon-channel targets) @@ -521,17 +524,16 @@ class DiscoveryScanEngine( } // Elect normal completion under the same mutex used by stopScan so a late stop cannot replace its outcome. - val request = - mutex.withLock { - collectorRegistry.collector = null - _scanState.value = DiscoveryScanState.Analysis - terminalRequestLocked( - pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_COMPLETE, - outcome = DiscoveryScanState.CompletionOutcome.Success, - awaitRestore = true, - generateAi = true, - ) - } + val request = mutex.withLock { + collectorRegistry.collector = null + _scanState.value = DiscoveryScanState.Analysis + terminalRequestLocked( + pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_COMPLETE, + outcome = DiscoveryScanState.CompletionOutcome.Success, + awaitRestore = true, + generateAi = true, + ) + } // complete() cancels scanScope before terminal cleanup finishes. This scan coroutine ends inside the call; // follow-up work belongs in terminal-coordinator callbacks, not after this invocation. terminalCoordinator.complete(request = request, generateAi = ::generateAiSummaries) @@ -539,20 +541,19 @@ class DiscoveryScanEngine( /** Common cleanup path when a scan step fails mid-loop. */ private suspend fun pauseAndAbort(persistPartialDwell: Boolean = false) { - val request = - mutex.withLock { - if (_scanState.value is DiscoveryScanState.Cancelling) { - null - } else { - _scanState.value = DiscoveryScanState.Analysis - terminalRequestLocked( - pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_FAILED, - outcome = DiscoveryScanState.CompletionOutcome.Failed, - awaitRestore = false, - generateAi = false, - ) - } + val request = mutex.withLock { + if (_scanState.value is DiscoveryScanState.Cancelling) { + null + } else { + _scanState.value = DiscoveryScanState.Analysis + terminalRequestLocked( + pendingStatus = DiscoverySessionStatus.RESTORE_PENDING_FAILED, + outcome = DiscoveryScanState.CompletionOutcome.Failed, + awaitRestore = false, + generateAi = false, + ) } + } if (request == null) return if (persistPartialDwell) { terminalCoordinator.complete(request = request, beforeFinalize = ::persistCurrentDwellResults) @@ -584,22 +585,26 @@ class DiscoveryScanEngine( ) Logger.i { "DiscoveryScanEngine: shifted to ${target.label} (use_preset=true)" } } else { - // Beacon custom-channel target: apply the offered preset+region, reset channel_num so firmware derives the - // frequency from the new name, then tune the primary channel to the offered name+PSK so nodes on that mesh - // are heard. The original primary channel is restored after the scan. + // Beacon custom-channel target: apply the offered preset+region, take the slot the mesh pinned or reset + // channel_num so firmware derives it from the new name, then tune the primary channel to the offered + // name+PSK so nodes on that mesh are heard. The original primary channel is restored after the scan. + val targetLora = + base + .newBuilder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = target.preset.modemPreset + wb.region = target.region ?: base.region + } + .build() + // Bound the pinned slot against the config we are about to apply, not the one we are leaving: a nonzero + // channel_num is taken verbatim, so an unaddressable slot would tune the scan to a frequency that does + // not exist. Zero puts us back on deriving it from the offered name. + val frequencySlot = target.frequencySlot?.takeIf { it in 1..targetLora.numChannels } ?: 0 radioController.setLocalConfig( Config.Builder() .also { wb -> - wb.lora = - base - .newBuilder() - .also { wb -> - wb.use_preset = true - wb.modem_preset = target.preset.modemPreset - wb.region = target.region ?: base.region - wb.channel_num = 0 - } - .build() + wb.lora = targetLora.newBuilder().also { lb -> lb.channel_num = frequencySlot }.build() } .build(), ) @@ -724,12 +729,8 @@ class DiscoveryScanEngine( val ni = NeighborInfo.ADAPTER.decodeOrNull(payload, Logger) ?: return for (neighbor in ni.neighbors) { val neighborNum = neighbor.node_id.toLong() - val node = - collectedNodes.getOrPut(neighborNum) { CollectedNodeData(nodeNum = neighborNum, neighborType = "mesh") } - // Only mark as mesh if not already seen directly - if (node.snr == 0f && node.rssi == null) { - node.neighborType = "mesh" - } + // Only a node not yet heard is added as mesh; one already heard directly keeps its type. + collectedNodes.getOrPut(neighborNum) { CollectedNodeData(nodeNum = neighborNum, neighborType = "mesh") } } } @@ -773,8 +774,8 @@ class DiscoveryScanEngine( // A null return means this device's session row is not in the active database and the dwell is unwritable. if (discoveryDao.insertDwellIfSessionExists(result, discoveredNodeEntities(), deviceAddress) == null) { Logger.w { - "DiscoveryScanEngine: session $sessionId for $deviceAddress is not in the active database; " + - "skipping dwell persistence" + "DiscoveryScanEngine: session $sessionId for ${deviceAddress.anonymize()} is not in the active " + + "database; skipping dwell persistence" } return@withLock } @@ -854,9 +855,11 @@ class DiscoveryScanEngine( userLat: Double, userLon: Double, ): DiscoveredNodeEntity { + val lat = latitude + val lon = longitude val distance = - if (hasValidCoordinates(latitude, longitude) && hasValidCoordinates(userLat, userLon)) { - latLongToMeter(userLat, userLon, latitude!!, longitude!!) + if (lat != null && lon != null && hasValidCoordinates(lat, lon) && hasValidCoordinates(userLat, userLon)) { + latLongToMeter(userLat, userLon, lat, lon) } else { null } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryViewModel.kt index 46ff2b2588..ad9a0e44b6 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryViewModel.kt @@ -23,10 +23,10 @@ import kotlinx.coroutines.flow.asStateFlow import org.koin.core.annotation.InjectedParam import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.LocaleUnitsProvider -import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity +import org.meshtastic.core.repository.DiscoveryRepository import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider @@ -40,7 +40,7 @@ import org.meshtastic.feature.discovery.scan.PresetRankingInput @KoinViewModel class DiscoverySummaryViewModel( @InjectedParam private val sessionId: Long, - private val discoveryDao: DiscoveryDao, + private val discoveryRepository: DiscoveryRepository, private val summaryGenerator: DiscoverySummaryGenerator, private val rankingEngine: DiscoveryRankingEngine, private val aiProvider: DiscoverySummaryAiProvider, @@ -49,10 +49,10 @@ class DiscoverySummaryViewModel( ) : ViewModel() { val session: StateFlow = - discoveryDao.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) + discoveryRepository.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null) val presetResults: StateFlow> = - discoveryDao.getPresetResultsFlow(sessionId).stateInWhileSubscribed(initialValue = emptyList()) + discoveryRepository.getPresetResultsFlow(sessionId).stateInWhileSubscribed(initialValue = emptyList()) private val _nodesByPreset = MutableStateFlow>>(emptyMap()) val nodesByPreset: StateFlow>> = _nodesByPreset.asStateFlow() @@ -82,12 +82,12 @@ class DiscoverySummaryViewModel( fun exportReport() { safeLaunch(tag = "exportReport") { val currentSession = - discoveryDao.getSession(sessionId) + discoveryRepository.getSession(sessionId) ?: run { _exportResult.value = ExportResult.Error("Session not found") return@safeLaunch } - val results = discoveryDao.getPresetResults(sessionId) + val results = discoveryRepository.getPresetResults(sessionId) val exportData = DiscoveryExportData( session = currentSession, @@ -110,26 +110,25 @@ class DiscoverySummaryViewModel( _aiSummary.value = null _presetAiSummaries.value = emptyMap() - val currentSession = discoveryDao.getSession(sessionId) ?: return@safeLaunch - val results = discoveryDao.getPresetResults(sessionId) + val currentSession = discoveryRepository.getSession(sessionId) ?: return@safeLaunch + val results = discoveryRepository.getPresetResults(sessionId) // Clear persisted AI summaries - discoveryDao.updateSession(currentSession.copy(aiSummary = null)) + discoveryRepository.updateSession(currentSession.copy(aiSummary = null)) for (result in results) { - discoveryDao.updatePresetResult(result.copy(aiSummary = null)) + discoveryRepository.updatePresetResult(result.copy(aiSummary = null)) } // Regenerate algorithmic _algorithmicSummary.value = summaryGenerator.generateSessionSummary(currentSession, results) // Recompute rankings - val rankingInputs = - results.map { result -> - PresetRankingInput( - presetResult = result, - discoveredNodes = _nodesByPreset.value[result.id].orEmpty(), - ) - } + val rankingInputs = results.map { result -> + PresetRankingInput( + presetResult = result, + discoveredNodes = _nodesByPreset.value[result.id].orEmpty(), + ) + } _rankings.value = rankingEngine.rank(rankingInputs) // Regenerate AI @@ -142,26 +141,24 @@ class DiscoverySummaryViewModel( private fun loadNodes() { safeLaunch(tag = "loadNodes") { - val results = discoveryDao.getPresetResults(sessionId) - val nodesMap = mutableMapOf>() - for (result in results) { - nodesMap[result.id] = discoveryDao.getDiscoveredNodes(result.id) - } + val results = discoveryRepository.getPresetResults(sessionId) + val nodesMap = discoveryRepository.getNodesByPresetResult(results.map { it.id }) _nodesByPreset.value = nodesMap // Compute deterministic rankings - val rankingInputs = - results.map { result -> - PresetRankingInput(presetResult = result, discoveredNodes = nodesMap[result.id].orEmpty()) - } + val rankingInputs = results.map { result -> + PresetRankingInput(presetResult = result, discoveredNodes = nodesMap[result.id].orEmpty()) + } _rankings.value = rankingEngine.rank(rankingInputs) // Load cached per-preset AI summaries val cachedPresetSummaries = - results.filter { !it.aiSummary.isNullOrBlank() }.associate { it.id to it.aiSummary!! } + results + .mapNotNull { result -> result.aiSummary?.takeUnless { it.isBlank() }?.let { result.id to it } } + .toMap() _presetAiSummaries.value = cachedPresetSummaries - val session = discoveryDao.getSession(sessionId) + val session = discoveryRepository.getSession(sessionId) if (session != null) { _algorithmicSummary.value = summaryGenerator.generateSessionSummary(session, results) @@ -187,7 +184,7 @@ class DiscoverySummaryViewModel( val summary = aiProvider.generateSessionSummary(session, results) if (summary != null) { _aiSummary.value = summary - discoveryDao.updateSession(session.copy(aiSummary = summary)) + discoveryRepository.updateSession(session.copy(aiSummary = summary)) } } } @@ -199,7 +196,7 @@ class DiscoverySummaryViewModel( val summary = aiProvider.generatePresetSummary(result) if (summary != null) { _presetAiSummaries.value = _presetAiSummaries.value + (result.id to summary) - discoveryDao.updatePresetResult(result.copy(aiSummary = summary)) + discoveryRepository.updatePresetResult(result.copy(aiSummary = summary)) } } } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt index a0cdfa14db..20014fc2d4 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt @@ -24,13 +24,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.core.model.ChannelOption import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.MeshBeaconOffer +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.model.util.isAlreadyJoined import org.meshtastic.core.repository.DiscoveryPrefs +import org.meshtastic.core.repository.DiscoveryRepository import org.meshtastic.core.repository.MeshBeaconRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository @@ -50,7 +51,7 @@ class DiscoveryViewModel( private val check24GhzCapability: Check24GhzCapability, private val meshBeaconRepository: MeshBeaconRepository, radioConfigRepository: RadioConfigRepository, - discoveryDao: DiscoveryDao, + discoveryRepository: DiscoveryRepository, ) : ViewModel() { val scanState: StateFlow = scanEngine.scanState @@ -119,7 +120,7 @@ class DiscoveryViewModel( .stateInWhileSubscribed(initialValue = false) val sessions: StateFlow> = - discoveryDao.getAllSessions().stateInWhileSubscribed(initialValue = emptyList()) + discoveryRepository.getAllSessions().stateInWhileSubscribed(initialValue = emptyList()) /** Beacon presets we've already auto-selected once, so a user's later deselection is never undone (FR-004). */ private val autoSelectedBeaconPresets = mutableSetOf() @@ -128,7 +129,7 @@ class DiscoveryViewModel( private val autoSelectedBeaconChannels = mutableSetOf() init { - safeLaunch(tag = "markInterruptedSessions") { discoveryDao.markInterruptedSessions() } + safeLaunch(tag = "markInterruptedSessions") { discoveryRepository.markInterruptedSessions() } safeLaunch(tag = "check24GhzCapability") { val result = check24GhzCapability() _is24GhzBlocked.value = @@ -216,13 +217,14 @@ class DiscoveryViewModel( } .build(), region = bc.region.takeIf { it != RegionCode.UNSET }, + frequencySlot = bc.frequencySlot, ) } val targets = presetTargets + channelTargets if (targets.isEmpty()) return@safeLaunch scanEngine.startScanTargets( targets = targets, - dwellDurationSeconds = dwellDurationMinutes.value.toLong() * SECONDS_PER_MINUTE, + dwellDurationSeconds = dwellDurationMinutes.value.toLong() * TimeConstants.SECONDS_PER_MINUTE, ) } } @@ -238,10 +240,6 @@ class DiscoveryViewModel( private fun restoreSelectedPresets(): Set = discoveryPrefs.selectedPresets.value .mapNotNull { name -> ChannelOption.entries.firstOrNull { it.name == name } } .toSet() - - companion object { - private const val SECONDS_PER_MINUTE = 60L - } } /** @@ -276,6 +274,7 @@ internal fun filterAlreadyJoinedBeaconChannels( psk = ch.psk, preset = ChannelOption.from(offer.beacon.offer_preset), region = offer.beacon.offer_region, + frequencySlot = offer.beacon.offer_frequency_slot?.takeIf { it > 0 }, ) } .distinctBy { it.id } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ScanTarget.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ScanTarget.kt index 115041fb53..963f4d963b 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ScanTarget.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ScanTarget.kt @@ -35,6 +35,12 @@ data class ScanTarget( val label: String, val channel: ChannelSettings? = null, val region: RegionCode? = null, + /** + * The frequency slot the advertising mesh pinned, when it advertised one. Null means derive the slot from + * [channel]'s name the way any node on it would — a mesh that pins a slot its name does not hash to would otherwise + * be scanned for on the wrong frequency and never heard. + */ + val frequencySlot: Int? = null, ) /** @@ -43,7 +49,15 @@ data class ScanTarget( * — but a different PSK is a different network, so it must not collapse into another row (that would send the user to * the wrong mesh). */ -data class BeaconChannel(val name: String, val psk: ByteString, val preset: ChannelOption?, val region: RegionCode) { +data class BeaconChannel( + val name: String, + val psk: ByteString, + val preset: ChannelOption?, + val region: RegionCode, + val frequencySlot: Int? = null, +) { + // The slot is part of the identity: the same name and PSK pinned to two different slots are two meshes on two + // frequencies, and collapsing them would silently scan only whichever row won. val id: String - get() = "$name|${preset?.name.orEmpty()}|${psk.hex()}" + get() = "$name|${preset?.name.orEmpty()}|${psk.hex()}|${frequencySlot ?: ""}" } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.kt index aa24910ab4..d3c48c5b72 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.kt @@ -17,24 +17,31 @@ package org.meshtastic.feature.discovery.export import org.meshtastic.core.common.util.DateFormatter +import org.meshtastic.core.common.util.DurationUnitLabels import org.meshtastic.core.common.util.MeasurementSystem import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.common.util.NumberFormatter +import org.meshtastic.core.common.util.formatDuration import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity +import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.util.toDistanceString -import org.meshtastic.feature.discovery.ui.formatDuration import kotlin.math.roundToInt +import kotlin.time.DurationUnit internal object DiscoveryReportFormatter { + // The report's labels are English, so its durations are too. + private fun reportDuration(totalSeconds: Long): String = + formatDuration(totalSeconds, DurationUnitLabels.English, smallest = DurationUnit.MINUTES) + fun formatSessionDate(session: DiscoverySessionEntity): String = DateFormatter.formatDateTime(session.timestamp) fun formatSessionOverviewLines(session: DiscoverySessionEntity): List> = listOf( "Date" to formatSessionDate(session), "Total unique nodes" to session.totalUniqueNodes.toString(), - "Total dwell time" to formatDuration(session.totalDwellSeconds), + "Total dwell time" to reportDuration(session.totalDwellSeconds), "Status" to session.completionStatus.replaceFirstChar { it.uppercase() }, "Channel utilization" to "${NumberFormatter.format(session.avgChannelUtilization, 1)}%", "Total messages" to session.totalMessages.toString(), @@ -45,7 +52,7 @@ internal object DiscoveryReportFormatter { add("Unique nodes" to result.uniqueNodes.toString()) add("Direct neighbors" to result.directNeighborCount.toString()) add("Mesh neighbors" to result.meshNeighborCount.toString()) - add("Dwell time" to formatDuration(result.dwellDurationSeconds)) + add("Dwell time" to reportDuration(result.dwellDurationSeconds)) add("Channel utilization" to "${NumberFormatter.format(result.avgChannelUtilization, 1)}%") add("Airtime rate" to "${NumberFormatter.format(result.avgAirtimeRate, 1)}%") add("Packet success" to "${NumberFormatter.format(result.packetSuccessRate, 1)}%") @@ -59,7 +66,7 @@ internal object DiscoveryReportFormatter { } fun formatNodeLine(node: DiscoveredNodeEntity, measurementSystem: MeasurementSystem): String = buildString { - append(node.longName ?: node.shortName ?: "!${node.nodeNum.toString(radix = 16)}") + append(node.longName ?: node.shortName ?: NodeAddress.numToDefaultId(node.nodeNum.toInt())) append(" | ${node.neighborType}") append(" | SNR: ${MetricFormatter.snr(node.snr)}") append(" | RSSI: ${MetricFormatter.rssi(node.rssi)}") diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaverLauncher.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaverLauncher.kt index 44c5af1869..68136ce105 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaverLauncher.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaverLauncher.kt @@ -17,14 +17,24 @@ package org.meshtastic.feature.discovery.export import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.meshtastic.core.ui.util.rememberFileExporter -/** - * Returns a launcher that saves [ExportResult.Success] content to the platform's file system. - * - * On Android this opens a SAF document-picker (ACTION_CREATE_DOCUMENT). On Desktop this writes to a user-chosen file - * via a file dialog. - */ -@Composable expect fun rememberExportSaver(): ExportSaverLauncher +/** Returns a launcher that saves [ExportResult.Success] content to a file the user picks. */ +@Composable +fun rememberExportSaver(): ExportSaverLauncher { + var pending by remember { mutableStateOf(null) } + val export = rememberFileExporter(content = { pending?.content }) + return remember(export) { + ExportSaverLauncher { result -> + pending = result + export(result.fileName, result.mimeType) + } + } +} /** Platform-agnostic handle for triggering a file-save from export data. */ fun interface ExportSaverLauncher { diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt index 7f1791f163..30c184589a 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt @@ -34,8 +34,11 @@ data class RankingScoreBreakdown( val neighborDiversity: Int, /** Criterion 3: non-duplicate packet count (numPacketsRx - numRxDupe). */ val nonDupePacketCount: Int, - /** Criterion 4a: median SNR across discovered nodes. */ - val medianSnr: Float, + /** + * Criterion 4a: median SNR across discovered nodes. Null when no node reported one, so nodes learned only from + * NeighborInfo never drag the median toward 0 dB. + */ + val medianSnr: Float?, /** * Criterion 4b: median RSSI across discovered nodes (tiebreak within criterion 4). Null when no node reported one — * 0 dBm would otherwise read as an excellent median and outrank real negative readings. @@ -95,8 +98,8 @@ class DiscoveryRankingEngine { val pr = presetResult val nodes = discoveredNodes - val snrValues = nodes.map { it.snr }.sorted() - // Nodes that never reported an rssi are excluded rather than dragged toward 0 dBm. + // Nodes that never reported a reading are excluded rather than dragged toward 0 dB / 0 dBm. + val snrValues = nodes.mapNotNull { it.snr }.sorted() val rssiValues = nodes.mapNotNull { it.rssi }.sorted() return ScoredPreset( @@ -106,7 +109,7 @@ class DiscoveryRankingEngine { uniqueNodeCount = pr.uniqueNodes, neighborDiversity = pr.directNeighborCount + pr.meshNeighborCount, nonDupePacketCount = (pr.numPacketsRx - pr.numRxDupe).coerceAtLeast(0), - medianSnr = median(snrValues) { it }, + medianSnr = medianFloat(snrValues), medianRssi = medianInt(rssiValues), bestKnownDistance = nodes.mapNotNull { it.distanceFromUser }.maxOrNull() ?: 0.0, failurePenalty = pr.packetFailureRate, @@ -162,12 +165,11 @@ class DiscoveryRankingEngine { cmp = b.breakdown.nonDupePacketCount.compareTo(a.breakdown.nonDupePacketCount) if (cmp != 0) return@Comparator cmp - // 4. Best median link quality: SNR first, then RSSI - cmp = b.breakdown.medianSnr.compareTo(a.breakdown.medianSnr) + // 4. Best median link quality: SNR first, then RSSI. Higher wins, but a preset where nobody reported + // a reading ranks after any measured preset rather than winning on a phantom 0 dB / 0 dBm. + cmp = compareDescendingMissingLast(a.breakdown.medianSnr, b.breakdown.medianSnr) if (cmp != 0) return@Comparator cmp - // Higher rssi wins, but a preset where nobody reported one ranks after any measured preset rather - // than winning on a phantom 0 dBm. - cmp = compareByRssiDescendingMissingLast(a.breakdown.medianRssi, b.breakdown.medianRssi) + cmp = compareDescendingMissingLast(a.breakdown.medianRssi, b.breakdown.medianRssi) if (cmp != 0) return@Comparator cmp // 5. Greatest best-known distance @@ -178,19 +180,19 @@ class DiscoveryRankingEngine { a.breakdown.failurePenalty.compareTo(b.breakdown.failurePenalty) } - /** Compute the median of a sorted float-convertible list. Returns 0 for empty. */ - internal fun median(sorted: List, toFloat: (T) -> Float): Float { - if (sorted.isEmpty()) return 0f + /** Compute the median of a sorted Float list. Returns null for empty: 0 is a real snr, not "no data". */ + private fun medianFloat(sorted: List): Float? { + if (sorted.isEmpty()) return null val mid = sorted.size / 2 return if (sorted.size % 2 == 0) { - (toFloat(sorted[mid - 1]) + toFloat(sorted[mid])) / 2f + (sorted[mid - 1] + sorted[mid]) / 2f } else { - toFloat(sorted[mid]) + sorted[mid] } } /** Orders two medians best-first, with an absent median always losing to a measured one. */ - private fun compareByRssiDescendingMissingLast(a: Int?, b: Int?): Int = when { + private fun > compareDescendingMissingLast(a: T?, b: T?): Int = when { a == b -> 0 a == null -> 1 b == null -> -1 diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryDetailScreen.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryDetailScreen.kt index d1495a1a9d..e3a9799a60 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryDetailScreen.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryDetailScreen.kt @@ -55,11 +55,13 @@ import org.meshtastic.core.resources.discovery_stat_total_dwell_time import org.meshtastic.core.resources.discovery_stat_total_messages import org.meshtastic.core.resources.discovery_stat_unique_nodes import org.meshtastic.core.resources.discovery_view_map +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.Map import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.discovery.DiscoveryHistoryDetailViewModel import org.meshtastic.feature.discovery.ui.component.PresetResultCard +import kotlin.time.DurationUnit @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -139,7 +141,7 @@ private fun SessionMetadataCard(session: DiscoverySessionEntity) { MetadataRow(stringResource(Res.string.discovery_stat_total_messages), session.totalMessages.toString()) MetadataRow( stringResource(Res.string.discovery_stat_total_dwell_time), - formatDuration(session.totalDwellSeconds), + formatDuration(session.totalDwellSeconds, smallest = DurationUnit.MINUTES), ) session.aiSummary?.let { summary -> Spacer(Modifier.height(8.dp)) diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryScreen.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryScreen.kt index 4a7a7527fd..30a98ea73a 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryScreen.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryScreen.kt @@ -18,7 +18,6 @@ package org.meshtastic.feature.discovery.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -51,6 +50,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.database.entity.DiscoverySessionEntity @@ -61,10 +61,12 @@ import org.meshtastic.core.resources.delete import org.meshtastic.core.resources.discovery_delete_session import org.meshtastic.core.resources.discovery_delete_session_confirm import org.meshtastic.core.resources.discovery_empty_history +import org.meshtastic.core.resources.discovery_empty_history_hint import org.meshtastic.core.resources.discovery_history import org.meshtastic.core.resources.discovery_scan_complete import org.meshtastic.core.resources.discovery_scan_incomplete -import org.meshtastic.core.resources.discovery_unique_nodes +import org.meshtastic.core.resources.discovery_unique_nodes_count +import org.meshtastic.core.ui.component.EmptyState import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.CheckCircle import org.meshtastic.core.ui.icon.Delete @@ -118,31 +120,33 @@ fun DiscoveryHistoryScreen( @Composable private fun EmptyHistoryState(modifier: Modifier = Modifier) { - Box(modifier = modifier, contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = MeshtasticIcons.History, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(Modifier.height(16.dp)) - Text( - text = stringResource(Res.string.discovery_empty_history), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } + EmptyState( + icon = MeshtasticIcons.History, + title = stringResource(Res.string.discovery_empty_history), + supportingText = stringResource(Res.string.discovery_empty_history_hint), + modifier = modifier, + ) } @Composable private fun SessionListItem(session: DiscoverySessionEntity, onClick: () -> Unit, onDelete: () -> Unit) { var showDeleteDialog by remember { mutableStateOf(false) } + val uniqueNodes = + pluralStringResource( + Res.plurals.discovery_unique_nodes_count, + session.totalUniqueNodes, + session.totalUniqueNodes, + ) + val status = + stringResource( + if (session.completionStatus == "complete") { + Res.string.discovery_scan_complete + } else { + Res.string.discovery_scan_incomplete + }, + ) val sessionDescription = - "${formatTimestamp(session.timestamp)}, ${session.presetsScanned}, " + - "${session.totalUniqueNodes} unique nodes, " + - if (session.completionStatus == "complete") "complete" else "incomplete" + listOf(formatTimestamp(session.timestamp), session.presetsScanned, uniqueNodes, status).joinToString(", ") Card( modifier = @@ -163,7 +167,7 @@ private fun SessionListItem(session: DiscoverySessionEntity, onClick: () -> Unit ) Spacer(Modifier.height(2.dp)) Text( - text = stringResource(Res.string.discovery_unique_nodes, session.totalUniqueNodes), + text = uniqueNodes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt index 6e415f7607..5d866e3a47 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt @@ -68,6 +68,7 @@ import org.meshtastic.core.resources.back import org.meshtastic.core.resources.discovery_analysing_results import org.meshtastic.core.resources.discovery_cancelling_scan import org.meshtastic.core.resources.discovery_connection_warning +import org.meshtastic.core.resources.discovery_dwell_minutes import org.meshtastic.core.resources.discovery_dwell_time import org.meshtastic.core.resources.discovery_dwell_time_description import org.meshtastic.core.resources.discovery_keep_screen_awake @@ -355,7 +356,7 @@ private fun DwellTimePicker( ) ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { if (enabled) expanded = it }) { OutlinedTextField( - value = "$selectedMinutes min", + value = stringResource(Res.string.discovery_dwell_minutes, selectedMinutes), onValueChange = {}, readOnly = true, enabled = enabled, @@ -365,7 +366,7 @@ private fun DwellTimePicker( ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { DWELL_OPTIONS.forEach { minutes -> DropdownMenuItem( - text = { Text("$minutes min") }, + text = { Text(stringResource(Res.string.discovery_dwell_minutes, minutes)) }, onClick = { onMinuteSelect(minutes) expanded = false diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoverySummaryScreen.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoverySummaryScreen.kt index 63a5d184ac..ca86b28dc5 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoverySummaryScreen.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoverySummaryScreen.kt @@ -66,6 +66,7 @@ import org.meshtastic.core.resources.discovery_stat_total_dwell_time import org.meshtastic.core.resources.discovery_stat_total_unique_nodes import org.meshtastic.core.resources.discovery_summary_not_available import org.meshtastic.core.resources.discovery_view_map +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.Map import org.meshtastic.core.ui.icon.MeshtasticIcons @@ -76,6 +77,7 @@ import org.meshtastic.feature.discovery.export.ExportResult import org.meshtastic.feature.discovery.export.rememberExportSaver import org.meshtastic.feature.discovery.scan.PresetRanking import org.meshtastic.feature.discovery.ui.component.PresetResultCard +import kotlin.time.DurationUnit @Composable fun DiscoverySummaryScreen( @@ -235,7 +237,7 @@ private fun SessionOverviewCard(session: DiscoverySessionEntity) { ) StatRow( label = stringResource(Res.string.discovery_stat_total_dwell_time), - value = formatDuration(session.totalDwellSeconds), + value = formatDuration(session.totalDwellSeconds, smallest = DurationUnit.MINUTES), ) StatRow( label = stringResource(Res.string.discovery_stat_status), @@ -312,10 +314,3 @@ internal fun StatRow(label: String, value: String, modifier: Modifier = Modifier Text(text = value, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) } } - -internal fun formatDuration(totalSeconds: Long): String { - val minutes = totalSeconds / 60 - val hours = minutes / 60 - val remainingMinutes = minutes % 60 - return if (hours > 0) "${hours}h ${remainingMinutes}m" else "${minutes}m" -} diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DwellProgressIndicator.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DwellProgressIndicator.kt index 15427f705e..ba7cc79c6b 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DwellProgressIndicator.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DwellProgressIndicator.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.semantics.progressBarRangeInfo import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.discovery_dwell_progress import org.meshtastic.core.resources.discovery_stat_dwelling_on @@ -39,7 +40,6 @@ import org.meshtastic.core.resources.discovery_time_remaining @Suppress("MagicNumber") private val CONTENT_PADDING = 8.dp -private const val SECONDS_PER_MINUTE = 60L /** Displays dwell progress for a single preset with a countdown timer and linear progress bar. */ @Composable @@ -55,8 +55,8 @@ fun DwellProgressIndicator( } else { 0f } - val minutes = remainingSeconds / SECONDS_PER_MINUTE - val seconds = remainingSeconds % SECONDS_PER_MINUTE + val minutes = remainingSeconds / TimeConstants.SECONDS_PER_MINUTE + val seconds = remainingSeconds % TimeConstants.SECONDS_PER_MINUTE val timeText = "${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}" val progressDescription = stringResource(Res.string.discovery_dwell_progress, presetName, timeText) diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/PresetResultCard.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/PresetResultCard.kt index 7252a7d0c1..e0cf04cdf9 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/PresetResultCard.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/PresetResultCard.kt @@ -46,8 +46,9 @@ import org.meshtastic.core.resources.discovery_stat_mesh import org.meshtastic.core.resources.discovery_stat_messages import org.meshtastic.core.resources.discovery_stat_sensor_pkts import org.meshtastic.core.resources.discovery_stat_unique_nodes +import org.meshtastic.core.resources.formatDuration import org.meshtastic.feature.discovery.ui.StatRow -import org.meshtastic.feature.discovery.ui.formatDuration +import kotlin.time.DurationUnit @Composable fun PresetResultCard( @@ -111,7 +112,7 @@ private fun PresetHeader(result: DiscoveryPresetResultEntity, rank: Int?, isTied } } Text( - text = formatDuration(result.dwellDurationSeconds), + text = formatDuration(result.dwellDurationSeconds, smallest = DurationUnit.MINUTES), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt index 865d4027f5..5283d1305f 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import kotlinx.coroutines.flow.first diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt index 7290fec1fb..1d22934892 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt @@ -14,18 +14,23 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.data.repository.DiscoveryRepositoryImpl import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.core.database.entity.DiscoverySessionStatus +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -39,6 +44,16 @@ import kotlin.test.assertTrue */ class DiscoveryMapFilterTest { + @BeforeTest + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + // region Preset filter selection @Test @@ -132,12 +147,51 @@ class DiscoveryMapFilterTest { // endregion + // region All-presets dedup + + @Test + fun allPresetsDedupKeepsMeasuredSnrOverUnheardSighting() = runTest { + val dao = SharedInMemoryDiscoveryDao() + val sessionId = dao.insertSession(testSession()) + val unheardPreset = + dao.insertPresetResult(DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "LONG_FAST")) + val measuredPreset = + dao.insertPresetResult(DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "SHORT_FAST")) + // The same node, named by NeighborInfo alone on one preset and heard at -10 dB on the other. + dao.insertDiscoveredNode(DiscoveredNodeEntity(presetResultId = unheardPreset, nodeNum = 7L)) + dao.insertDiscoveredNode(DiscoveredNodeEntity(presetResultId = measuredPreset, nodeNum = 7L, snr = -10f)) + + val nodes = mapViewModel(sessionId, dao).filteredNodes.first { it.isNotEmpty() } + + assertEquals(listOf(measuredPreset), nodes.map { it.presetResultId }) + assertEquals(-10f, nodes.single().snr) + } + + @Test + fun allPresetsDedupKeepsZeroDbReadingOverNegativeReading() = runTest { + val dao = SharedInMemoryDiscoveryDao() + val sessionId = dao.insertSession(testSession()) + val zeroPreset = + dao.insertPresetResult(DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "LONG_FAST")) + val negativePreset = + dao.insertPresetResult(DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "SHORT_FAST")) + dao.insertDiscoveredNode(DiscoveredNodeEntity(presetResultId = negativePreset, nodeNum = 7L, snr = -10f)) + dao.insertDiscoveredNode(DiscoveredNodeEntity(presetResultId = zeroPreset, nodeNum = 7L, snr = 0f)) + + val nodes = mapViewModel(sessionId, dao).filteredNodes.first { it.isNotEmpty() } + + assertEquals(listOf(zeroPreset), nodes.map { it.presetResultId }) + assertEquals(0f, nodes.single().snr) + } + + // endregion + // region Helpers - private fun createViewModel(): DiscoveryMapViewModel { - val dao = SharedInMemoryDiscoveryDao() - return DiscoveryMapViewModel(sessionId = 1L, discoveryDao = dao) - } + private fun createViewModel(): DiscoveryMapViewModel = mapViewModel(sessionId = 1L, SharedInMemoryDiscoveryDao()) + + private fun mapViewModel(sessionId: Long, dao: SharedInMemoryDiscoveryDao) = + DiscoveryMapViewModel(sessionId = sessionId, discoveryRepository = DiscoveryRepositoryImpl(dao)) private fun testSession() = DiscoverySessionEntity( timestamp = 1_000_000L, diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt index c45e6f95f3..e48054b901 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import kotlinx.coroutines.SupervisorJob @@ -222,6 +220,47 @@ class DiscoveryPacketCollectionTest { val meshNode = nodes.find { it.nodeNum == 5555L } assertTrue(meshNode != null, "Neighbor-info-only node should be persisted") assertEquals("mesh", meshNode.neighborType, "Neighbor-info-only node should have 'mesh' type") + assertNull(meshNode.snr, "A node never heard directly has no SNR, not 0 dB") + } + + @Test + fun neighborInfoDoesNotDemoteDirectNodeHeardAtZeroDb() = runTest { + val engine = createEngine(this) + nodeRepository.setMyNodeInfo(createMyNodeInfo()) + engine.startScan(testPresets, dwellDurationSeconds = 60) + awaitDwell(engine) + + engine.onPacketReceived( + positionPacket(from = 6667, latI = 377749000, lonI = -1224194000, snr = 0f, rssi = null), + dataPacket(from = 6667), + ) + engine.onPacketReceived(neighborInfoPacket(from = 8888, neighborNodeIds = listOf(6667)), dataPacket(8888)) + + engine.stopScan() + + val directNode = discoveryDao.discoveredNodes.values.single { it.nodeNum == 6667L } + assertEquals("direct", directNode.neighborType, "A 0 dB reading is a direct sighting") + assertEquals(0f, directNode.snr) + } + + @Test + fun directPacketUpgradesANodeFirstNamedByNeighborInfo() = runTest { + val engine = createEngine(this) + nodeRepository.setMyNodeInfo(createMyNodeInfo()) + engine.startScan(testPresets, dwellDurationSeconds = 60) + awaitDwell(engine) + + engine.onPacketReceived(neighborInfoPacket(from = 8888, neighborNodeIds = listOf(6668)), dataPacket(8888)) + engine.onPacketReceived( + positionPacket(from = 6668, latI = 377749000, lonI = -1224194000, snr = -4f, rssi = -90), + dataPacket(from = 6668), + ) + + engine.stopScan() + + val node = discoveryDao.discoveredNodes.values.single { it.nodeNum == 6668L } + assertEquals("direct", node.neighborType, "Hearing the node itself makes it a direct sighting") + assertEquals(-4f, node.snr) } @Test @@ -290,7 +329,7 @@ class DiscoveryPacketCollectionTest { deviceId = "test-device", ) - private fun positionPacket(from: Int, latI: Int, lonI: Int, snr: Float = 5.5f, rssi: Int = -70): MeshPacket { + private fun positionPacket(from: Int, latI: Int, lonI: Int, snr: Float = 5.5f, rssi: Int? = -70): MeshPacket { val posPayload = Position.ADAPTER.encode( Position.Builder() diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryRankingEngineTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryRankingEngineTest.kt index 70831e60f1..fb4afb655b 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryRankingEngineTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryRankingEngineTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import org.meshtastic.core.database.entity.DiscoveredNodeEntity @@ -59,7 +57,7 @@ class DiscoveryRankingEngineTest { private fun node( presetResultId: Long = 1, nodeNum: Long = 1, - snr: Float = 0f, + snr: Float? = null, rssi: Int? = 0, distanceFromUser: Double? = null, ) = DiscoveredNodeEntity( @@ -369,11 +367,11 @@ class DiscoveryRankingEngineTest { } @Test - fun noNodesProducesZeroMediansAndDistance() { + fun noNodesProducesNoMediansAndZeroDistance() { val p = preset(uniqueNodes = 3, numPacketsRx = 20) val result = engine.rank(listOf(input(p, emptyList()))) - assertEquals(0f, result[0].scoreBreakdown.medianSnr) + assertNull(result[0].scoreBreakdown.medianSnr, "no nodes means no snr median, not 0 dB") assertNull(result[0].scoreBreakdown.medianRssi, "no nodes means no rssi median, not 0 dBm") assertEquals(0.0, result[0].scoreBreakdown.bestKnownDistance) } @@ -392,6 +390,51 @@ class DiscoveryRankingEngineTest { assertNull(result[1].scoreBreakdown.medianRssi) } + @Test + fun unheardNodesDoNotPullTheSnrMedianTowardZero() { + val pA = preset(id = 1, name = "mostlyUnheard", uniqueNodes = 3, numPacketsRx = 50) + val pB = preset(id = 2, name = "measured", uniqueNodes = 3, numPacketsRx = 50) + // A heard one node at -10 dB and learned two more only from NeighborInfo, which reports no SNR for them. + val nodesA = + listOf( + node(presetResultId = 1, nodeNum = 1, snr = -10f), + node(presetResultId = 1, nodeNum = 2), + node(presetResultId = 1, nodeNum = 3), + ) + val nodesB = listOf(node(presetResultId = 2, nodeNum = 4, snr = -5f)) + + val result = engine.rank(listOf(input(pA, nodesA), input(pB, nodesB))) + + assertEquals("measured", result[0].presetResult.presetName) + assertEquals(-10f, result[1].scoreBreakdown.medianSnr) + } + + @Test + fun presetWithNoSnrReadingsRanksAfterMeasuredPreset() { + val pA = preset(id = 1, name = "unheard", uniqueNodes = 2, numPacketsRx = 50) + val pB = preset(id = 2, name = "measured", uniqueNodes = 2, numPacketsRx = 50) + val nodesA = List(2) { node(presetResultId = 1, nodeNum = it + 1L) } + val nodesB = List(2) { node(presetResultId = 2, nodeNum = it + 3L, snr = -15f) } + + val result = engine.rank(listOf(input(pA, nodesA), input(pB, nodesB))) + + assertEquals("measured", result[0].presetResult.presetName, "an absent snr median must not win on 0 dB") + assertNull(result[1].scoreBreakdown.medianSnr) + } + + @Test + fun zeroDbSnrReadingsRankAsMeasured() { + val pA = preset(id = 1, name = "zeroDb", uniqueNodes = 2, numPacketsRx = 50) + val pB = preset(id = 2, name = "negative", uniqueNodes = 2, numPacketsRx = 50) + val nodesA = List(2) { node(presetResultId = 1, nodeNum = it + 1L, snr = 0f) } + val nodesB = List(2) { node(presetResultId = 2, nodeNum = it + 3L, snr = -5f) } + + val result = engine.rank(listOf(input(pB, nodesB), input(pA, nodesA))) + + assertEquals("zeroDb", result[0].presetResult.presetName) + assertEquals(0f, result[0].scoreBreakdown.medianSnr) + } + @Test fun nodesWithoutDistanceYieldZeroBestDistance() { val p = preset(id = 1, uniqueNodes = 2) diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt index 2aa0dc5e03..6804a77a45 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import kotlinx.coroutines.CompletableDeferred @@ -268,7 +266,6 @@ class DiscoveryScanEngineTest { * (collection clearing). Call before sending packets to avoid a race where the scan loop's `collectedNodes.clear()` * wipes out test-injected data. */ - @Suppress("MagicNumber") private suspend fun awaitScanLoopInit() { delay(100) } @@ -771,7 +768,7 @@ class DiscoveryScanEngineTest { val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome) assertEquals(1, radioController.neighborInfoRequests.size) assertEquals("complete", discoveryDao.sessions.values.single().completionStatus) } @@ -787,7 +784,7 @@ class DiscoveryScanEngineTest { val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome) assertEquals(1, radioController.neighborInfoRequests.size) assertEquals("complete", discoveryDao.sessions.values.single().completionStatus) } @@ -809,7 +806,7 @@ class DiscoveryScanEngineTest { // none of which happened before the fix, because cancelScanInternal() cancelled this coroutine first. val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Failed, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Failed, state.outcome) assertFalse(engine.isActive) assertNull(collectorRegistry.collector, "collector should be unregistered") @@ -928,7 +925,7 @@ class DiscoveryScanEngineTest { val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome) assertEquals(DiscoverySessionStatus.COMPLETE, discoveryDao.sessions.values.first().completionStatus) } @@ -1002,7 +999,7 @@ class DiscoveryScanEngineTest { val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Failed, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Failed, state.outcome) assertEquals(DiscoverySessionStatus.FAILED, discoveryDao.sessions.values.first().completionStatus) assertEquals(ChannelOption.LONG_FAST.modemPreset, radioController.lastLocalConfig?.lora?.modem_preset) assertTrue( @@ -1162,7 +1159,7 @@ class DiscoveryScanEngineTest { val state = engine.scanState.value assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state") - assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome) + assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome) assertEquals(DiscoverySessionStatus.COMPLETE, discoveryDao.sessions.values.first().completionStatus) assertEquals(ChannelOption.LONG_FAST.modemPreset, radioController.lastLocalConfig?.lora?.modem_preset) } diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryAiProviderTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryAiProviderTest.kt index 0f7cc86cb8..cb526723ac 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryAiProviderTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryAiProviderTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import kotlinx.coroutines.test.runTest diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt index b8a33f364e..39c477d95c 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.discovery import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt index 7e401d17b7..64cc3ccf87 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt @@ -221,6 +221,11 @@ internal class SharedInMemoryDiscoveryDao : DiscoveryDao { override fun getDiscoveredNodesFlow(presetResultId: Long): Flow> = discoveredNodesFlow.map { nodes -> nodes.filter { it.presetResultId == presetResultId } } + override suspend fun getDiscoveredNodesForPresetResults(presetResultIds: List): List = + stateLock.withLock { + mutableDiscoveredNodes.values.filter { it.presetResultId in presetResultIds }.sortedBy { it.id } + } + override suspend fun getUniqueNodeNums(sessionId: Long): List = stateLock.withLock { mutablePresetResults.values .filter { it.sessionId == sessionId } diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatterTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatterTest.kt new file mode 100644 index 0000000000..c06df868be --- /dev/null +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatterTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.discovery.export + +import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.common.util.MetricFormatter +import org.meshtastic.core.database.entity.DiscoveredNodeEntity +import kotlin.test.Test +import kotlin.test.assertTrue + +class DiscoveryReportFormatterTest { + + @Test + fun nodeLineMarksAnUnheardNodeSnrAsUnknown() { + // A node named only by NeighborInfo is stored with the entity's default SNR. + val line = DiscoveryReportFormatter.formatNodeLine(node(), MeasurementSystem.METRIC) + + assertTrue("SNR: ${MetricFormatter.snr(null)} |" in line, line) + } + + @Test + fun nodeLinePrintsAZeroDbSnrAsAReading() { + val line = DiscoveryReportFormatter.formatNodeLine(node().copy(snr = 0f), MeasurementSystem.METRIC) + + assertTrue("SNR: ${MetricFormatter.snr(0f)} |" in line, line) + } + + private fun node() = DiscoveredNodeEntity(presetResultId = 1L, nodeNum = 0x1234L, longName = "Relay") +} diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportNodeIdTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportNodeIdTest.kt new file mode 100644 index 0000000000..ef78929da4 --- /dev/null +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportNodeIdTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.discovery.export + +import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.database.entity.DiscoveredNodeEntity +import kotlin.test.Test +import kotlin.test.assertTrue + +class DiscoveryReportNodeIdTest { + + @Test + fun `an unnamed node is listed by its padded node id`() { + val line = + DiscoveryReportFormatter.formatNodeLine( + DiscoveredNodeEntity(presetResultId = 1L, nodeNum = 0x1234L), + MeasurementSystem.METRIC, + ) + + assertTrue(line.startsWith("!00001234 |"), line) + } + + @Test + fun `a node number above the signed range keeps its unsigned id`() { + val line = + DiscoveryReportFormatter.formatNodeLine( + DiscoveredNodeEntity(presetResultId = 1L, nodeNum = 0xFFFF_FFFEL), + MeasurementSystem.METRIC, + ) + + assertTrue(line.startsWith("!fffffffe |"), line) + } +} diff --git a/feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.jvm.kt b/feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.jvm.kt deleted file mode 100644 index 35be6d0956..0000000000 --- a/feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/export/ExportSaver.jvm.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.discovery.export - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import javax.swing.JFileChooser -import javax.swing.filechooser.FileNameExtensionFilter - -@Composable -actual fun rememberExportSaver(): ExportSaverLauncher { - val scope = rememberCoroutineScope() - return ExportSaverLauncher { result -> - scope.launch { - withContext(Dispatchers.IO) { - @Suppress("TooGenericExceptionCaught") - try { - val chooser = - JFileChooser().apply { - dialogTitle = "Save Discovery Report" - selectedFile = File(result.fileName) - val ext = result.fileName.substringAfterLast('.', "txt") - fileFilter = FileNameExtensionFilter("${ext.uppercase()} files", ext) - } - if (chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { - chooser.selectedFile.writeBytes(result.content) - } - } catch (e: Exception) { - Logger.e(throwable = e) { "Failed to save export file on desktop" } - } - } - } - } -} diff --git a/feature/docs/README.md b/feature/docs/README.md index 9014dd8112..0a14352522 100644 --- a/feature/docs/README.md +++ b/feature/docs/README.md @@ -134,8 +134,7 @@ feature:docs ├── core:common, core:navigation, core:resources, core:ui, core:di ├── coil (image loading in Markdown) ├── markdown-renderer-m3 (Compose Markdown rendering) - ├── compose.material3.adaptive, compose.material3.adaptive.navigation3 - └── kotlinx.collections.immutable + └── compose.material3.adaptive, compose.material3.adaptive.navigation3 ``` diff --git a/feature/docs/build.gradle.kts b/feature/docs/build.gradle.kts index bed5fdd299..e411f3e6a3 100644 --- a/feature/docs/build.gradle.kts +++ b/feature/docs/build.gradle.kts @@ -18,10 +18,10 @@ plugins { alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.kmp.jvm.android) } kotlin { + // No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT. android { namespace = "org.meshtastic.feature.docs" androidResources.enable = true @@ -35,7 +35,6 @@ kotlin { implementation(projects.core.ui) implementation(projects.core.di) - implementation(libs.kotlinx.collections.immutable) implementation(libs.jetbrains.compose.material3.adaptive) implementation(libs.jetbrains.compose.material3.adaptive.navigation3) implementation(libs.coil) diff --git a/feature/docs/detekt-baseline.xml b/feature/docs/detekt-baseline.xml new file mode 100644 index 0000000000..9c35d5f9c2 --- /dev/null +++ b/feature/docs/detekt-baseline.xml @@ -0,0 +1,13 @@ + + + + + AbstractClassCanBeInterface:DocTranslationService.kt:DownloadResult$DownloadResult + AbstractClassCanBeInterface:DocTranslationService.kt:TranslationResult$TranslationResult + AbstractClassCanBeInterface:MarkdownTranslationSegmenter.kt:MarkdownTranslationSegmenter.Segment$Segment + LongParameterList:DocsNavigation.kt:ChirpyUiState + UnnecessaryLaunchedEffect:DocsNavigation.kt:LaunchedEffect + UnusedPrivateProperty:ChirpyAssistantSheet.kt:private val AVATAR_SIZE = 24.dp + UseOrEmpty:DocsNavigation.kt:loaded.markdown ?: "" + + diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ai/KeywordFallbackAssistant.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ai/KeywordFallbackAssistant.kt index 582e82081d..591f45c4d3 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ai/KeywordFallbackAssistant.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ai/KeywordFallbackAssistant.kt @@ -19,6 +19,9 @@ package org.meshtastic.feature.docs.ai import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Single +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.chirpy_fallback_unavailable +import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.feature.docs.data.KeywordSearchEngine import org.meshtastic.feature.docs.model.AIDocAssistantResult import org.meshtastic.feature.docs.model.ModelReadiness @@ -35,7 +38,7 @@ class KeywordFallbackAssistant(private val searchEngine: KeywordSearchEngine) : val pages = searchEngine.selectForTokenBudget(question, maxChars = 20_000) return if (pages.isNotEmpty()) { AIDocAssistantResult.Fallback( - message = "AI assistant is not available on this platform. Here are pages that may help:", + message = getStringSuspend(Res.string.chirpy_fallback_unavailable), suggestedPages = pages, ) } else { diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt index 1f9a291e90..c582ce0efc 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.feature.docs.data +import kotlinx.coroutines.CancellationException import meshtasticandroid.feature.docs.generated.resources.Res import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString @@ -33,6 +34,7 @@ import org.meshtastic.core.resources.doc_keywords_messages import org.meshtastic.core.resources.doc_keywords_mqtt import org.meshtastic.core.resources.doc_keywords_node_metrics import org.meshtastic.core.resources.doc_keywords_nodes +import org.meshtastic.core.resources.doc_keywords_notifications import org.meshtastic.core.resources.doc_keywords_onboarding import org.meshtastic.core.resources.doc_keywords_settings_module import org.meshtastic.core.resources.doc_keywords_settings_radio @@ -54,6 +56,7 @@ import org.meshtastic.core.resources.doc_title_messages import org.meshtastic.core.resources.doc_title_mqtt import org.meshtastic.core.resources.doc_title_node_metrics import org.meshtastic.core.resources.doc_title_nodes +import org.meshtastic.core.resources.doc_title_notifications import org.meshtastic.core.resources.doc_title_onboarding import org.meshtastic.core.resources.doc_title_settings_module import org.meshtastic.core.resources.doc_title_settings_radio @@ -154,6 +157,8 @@ class DefaultDocBundleLoader : DocBundleLoader { try { val bytes = Res.readBytes(localePath) return stripFrontmatter(bytes.decodeToString()) + } catch (e: CancellationException) { + throw e } catch (_: Exception) { continue } @@ -174,6 +179,8 @@ class DefaultDocBundleLoader : DocBundleLoader { try { Res.readBytes(localePath) true + } catch (e: CancellationException) { + throw e } catch (_: Exception) { false } @@ -204,6 +211,8 @@ class DefaultDocBundleLoader : DocBundleLoader { val bytes = Res.readBytes(resourcePath) val raw = bytes.decodeToString() stripFrontmatter(raw) + } catch (e: CancellationException) { + throw e } catch (_: Exception) { "# ${page.title}\n\nContent not available. The documentation file could not be loaded." } @@ -423,6 +432,16 @@ class DefaultDocBundleLoader : DocBundleLoader { 3700, "translate", ), + UserPageDef( + "notifications", + CoreRes.string.doc_title_notifications, + CoreRes.string.doc_keywords_notifications, + "en/user/notifications.html", + 18, + listOf("notifications", "notification-channels", "wear-os", "smartwatch"), + 3600, + "notifications", + ), UserPageDef( "app-functions", CoreRes.string.doc_title_app_functions, diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/navigation/DocsNavigation.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/navigation/DocsNavigation.kt index 3b7cd64f91..88ece1b5b5 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/navigation/DocsNavigation.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/navigation/DocsNavigation.kt @@ -20,11 +20,11 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey @@ -91,7 +91,7 @@ private fun rememberChirpyState( val aiAssistant = koinInject() val holder = koinInject() - val modelReadiness by aiAssistant.modelStatus.collectAsState() + val modelReadiness by aiAssistant.modelStatus.collectAsStateWithLifecycle() var isSupported by remember { mutableStateOf(false) } // Trigger initial availability check and model download. @@ -140,12 +140,6 @@ private fun DocsHelpScreen(backStack: NavBackStack, chirpy: ChirpyUiStat var isLoading by remember { mutableStateOf(true) } var searchQuery by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - val bundle = bundleLoader.load() - pages = bundle.pages.sortedWith(compareBy({ it.section.toString() }, { it.navOrder })) - isLoading = false - } - LaunchedEffect(searchQuery) { if (searchQuery.isBlank()) { val bundle = bundleLoader.load() @@ -154,6 +148,7 @@ private fun DocsHelpScreen(backStack: NavBackStack, chirpy: ChirpyUiStat val results = searchEngine.search(searchQuery) pages = results.map { it.page } } + isLoading = false } val backHandlerState = rememberNavigationEventState(NavigationEventInfo.None) diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ChirpyAssistantSheet.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ChirpyAssistantSheet.kt index 5f3109cb12..408c440724 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ChirpyAssistantSheet.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ChirpyAssistantSheet.kt @@ -71,6 +71,7 @@ import androidx.compose.ui.unit.dp import com.mikepenz.markdown.m3.Markdown import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.chirpy import org.meshtastic.core.resources.chirpy_assistant_title import org.meshtastic.core.resources.chirpy_checking import org.meshtastic.core.resources.chirpy_downloading @@ -78,6 +79,7 @@ import org.meshtastic.core.resources.chirpy_downloading_subtitle import org.meshtastic.core.resources.chirpy_search_placeholder import org.meshtastic.core.resources.chirpy_thinking import org.meshtastic.core.resources.img_chirpy +import org.meshtastic.core.resources.send import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Send import org.meshtastic.feature.docs.model.AIDocAssistantSessionState @@ -178,7 +180,10 @@ private fun ChirpyChatSheet( keyboardActions = KeyboardActions(onSend = { doSend() }), trailingIcon = { IconButton(onClick = ::doSend, enabled = canSend) { - Icon(imageVector = MeshtasticIcons.Send, contentDescription = "Send") + Icon( + imageVector = MeshtasticIcons.Send, + contentDescription = stringResource(CoreRes.string.send), + ) } }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), @@ -325,7 +330,7 @@ private fun ChirpyChip(modifier: Modifier = Modifier) { ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Chirpy", + text = stringResource(CoreRes.string.chirpy), fontSize = MaterialTheme.typography.labelLarge.fontSize, textAlign = TextAlign.Center, maxLines = 1, diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt index 0ad48ad7e2..c9f56c0520 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt @@ -17,13 +17,13 @@ package org.meshtastic.feature.docs.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.painter.Painter +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImagePainter import coil3.compose.LocalPlatformContext import coil3.compose.rememberAsyncImagePainter @@ -96,7 +96,7 @@ class ComposeResourceImageTransformer : ImageTransformer { override fun intrinsicSize(painter: Painter): Size { var size by remember(painter) { mutableStateOf(painter.intrinsicSize) } if (painter is AsyncImagePainter) { - val painterState = painter.state.collectAsState() + val painterState = painter.state.collectAsStateWithLifecycle() painterState.value.painter?.intrinsicSize?.also { size = it } } return size diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt index 3ea7f96f5b..5b4ce4ff38 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt @@ -34,6 +34,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Message import org.meshtastic.core.ui.icon.Nodes import org.meshtastic.core.ui.icon.Notes +import org.meshtastic.core.ui.icon.Notifications import org.meshtastic.core.ui.icon.PersonSearch import org.meshtastic.core.ui.icon.PinDrop import org.meshtastic.core.ui.icon.Rssi @@ -82,6 +83,8 @@ internal fun DocPage.resolveIcon(): ImageVector = when (iconId) { "translate" -> MeshtasticIcons.Language + "notifications" -> MeshtasticIcons.Notifications + "app-functions" -> MeshtasticIcons.Api "widget" -> MeshtasticIcons.Chart diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt index 50ffb192e3..fb0472ca3b 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt @@ -50,6 +50,11 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.doc_loading import org.meshtastic.core.resources.doc_no_documentation import org.meshtastic.core.resources.doc_no_results +import org.meshtastic.core.resources.doc_open_page +import org.meshtastic.core.resources.doc_section_developer +import org.meshtastic.core.resources.doc_section_user +import org.meshtastic.core.resources.help_and_documentation +import org.meshtastic.core.resources.navigate_back import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.docs.model.AIDocAssistantSessionState @@ -85,10 +90,13 @@ fun DocsBrowserScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Help & Documentation") }, + title = { Text(stringResource(Res.string.help_and_documentation)) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(imageVector = MeshtasticIcons.ArrowBack, contentDescription = "Navigate back") + Icon( + imageVector = MeshtasticIcons.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + ) } }, ) @@ -196,7 +204,7 @@ private fun DocsTocList(pages: List, onSelectPage: (String) -> Unit, mo if (userGuidePages.isNotEmpty()) { item { Text( - text = "User Guide", + text = stringResource(Res.string.doc_section_user), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp).semantics { heading() }, ) @@ -208,7 +216,7 @@ private fun DocsTocList(pages: List, onSelectPage: (String) -> Unit, mo item { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) Text( - text = "Developer Guide", + text = stringResource(Res.string.doc_section_developer), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp).semantics { heading() }, ) @@ -220,6 +228,7 @@ private fun DocsTocList(pages: List, onSelectPage: (String) -> Unit, mo @Composable private fun DocPageListItem(page: DocPage, onSelectPage: (String) -> Unit, modifier: Modifier = Modifier) { + val openPageLabel = stringResource(Res.string.doc_open_page, page.title) ListItem( leadingContent = { Icon( @@ -229,6 +238,6 @@ private fun DocPageListItem(page: DocPage, onSelectPage: (String) -> Unit, modif ) }, headlineContent = { Text(page.title) }, - modifier = modifier.clickable { onSelectPage(page.id) }.semantics { contentDescription = "Open ${page.title}" }, + modifier = modifier.clickable { onSelectPage(page.id) }.semantics { contentDescription = openPageLabel }, ) } diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreen.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreen.kt index fe26c30c58..cd30583784 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreen.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreen.kt @@ -45,6 +45,15 @@ import com.mikepenz.markdown.compose.elements.MarkdownTableHeader import com.mikepenz.markdown.compose.elements.MarkdownTableRow import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.model.markdownDimens +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.doc_auto_translated +import org.meshtastic.core.resources.doc_community_translated +import org.meshtastic.core.resources.doc_no_content +import org.meshtastic.core.resources.doc_page_not_found +import org.meshtastic.core.resources.doc_page_not_found_detail +import org.meshtastic.core.resources.documentation +import org.meshtastic.core.resources.navigate_back import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.docs.model.AIDocAssistantSessionState @@ -81,7 +90,7 @@ fun DocsPageRouteScreen( title = { Column { Text( - text = content?.page?.title ?: "Documentation", + text = content?.page?.title ?: stringResource(Res.string.documentation), maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -89,8 +98,8 @@ fun DocsPageRouteScreen( Text( text = when (translationSource) { - TranslationSource.ML_KIT -> "Auto-translated" - TranslationSource.BUNDLED -> "Community translated" + TranslationSource.ML_KIT -> stringResource(Res.string.doc_auto_translated) + TranslationSource.BUNDLED -> stringResource(Res.string.doc_community_translated) }, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -100,7 +109,10 @@ fun DocsPageRouteScreen( }, navigationIcon = { IconButton(onClick = onBack) { - Icon(imageVector = MeshtasticIcons.ArrowBack, contentDescription = "Navigate back") + Icon( + imageVector = MeshtasticIcons.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + ) } }, ) @@ -130,9 +142,12 @@ fun DocsPageRouteScreen( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - Text(text = "Page not found: $pageId", style = MaterialTheme.typography.bodyLarge) Text( - text = "This page may have been moved or removed.", + text = stringResource(Res.string.doc_page_not_found, pageId), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(Res.string.doc_page_not_found_detail), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp), ) @@ -140,7 +155,7 @@ fun DocsPageRouteScreen( } else -> { - val markdownText = content.markdown ?: "No content available." + val markdownText = content.markdown ?: stringResource(Res.string.doc_no_content) val platformUriHandler = LocalUriHandler.current val docsUriHandler = remember(platformUriHandler) { diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt index f0b970c952..545c403bc4 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt @@ -17,42 +17,16 @@ package org.meshtastic.feature.docs.ui import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.foundation.text.input.placeCursorAtEnd -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.ExpandedFullScreenSearchBar import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarScrollBehavior -import androidx.compose.material3.SearchBarState -import androidx.compose.material3.SearchBarValue -import androidx.compose.material3.Text -import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.doc_clear_search import org.meshtastic.core.resources.doc_search_placeholder -import org.meshtastic.core.resources.navigate_back -import org.meshtastic.core.ui.icon.ArrowBack -import org.meshtastic.core.ui.icon.Close -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Search +import org.meshtastic.core.ui.component.MeshtasticSearchBar +import org.meshtastic.core.ui.component.SEARCH_BAR_EXPANDED_TAG_SUFFIX /** * Tag for the collapsed field; distinct from [DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG] so tests can target it alone. @@ -60,14 +34,9 @@ import org.meshtastic.core.ui.icon.Search const val DOCS_SEARCH_BAR_INPUT_FIELD_TAG = "DocsSearchBarInputField" /** Tag for the field instance the full-screen overlay shows once expanded. */ -const val DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG = "DocsSearchBarExpandedInputField" +const val DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG = DOCS_SEARCH_BAR_INPUT_FIELD_TAG + SEARCH_BAR_EXPANDED_TAG_SUFFIX -/** - * Search bar for filtering documentation pages by keywords. - * - * A real M3 [SearchBar] driven by [rememberSearchBarState]: collapsed it sits inline above the results, and focusing it - * expands to a full-screen overlay showing [expandedContent] under the same field. - */ +/** Search bar for filtering documentation pages by keywords. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun DocsSearchBar( @@ -77,95 +46,14 @@ fun DocsSearchBar( scrollBehavior: SearchBarScrollBehavior? = null, expandedContent: @Composable ColumnScope.() -> Unit = {}, ) { - val searchPlaceholder = stringResource(Res.string.doc_search_placeholder) - val clearSearchDescription = stringResource(Res.string.doc_clear_search) - val backDescription = stringResource(Res.string.navigate_back) - val textFieldState = rememberTextFieldState(query) - val searchBarState = rememberSearchBarState() - val latestOnQueryChange by rememberUpdatedState(onQueryChange) - - // A caller-driven query change (e.g. an external reset) must be mirrored into the field. - LaunchedEffect(query, textFieldState) { - if (textFieldState.text.toString() != query) { - textFieldState.edit { - replace(0, length, query) - placeCursorAtEnd() - } - } - } - // Edits made through the field are reported back to the caller. - LaunchedEffect(textFieldState) { - snapshotFlow { textFieldState.text.toString() }.collect { latestOnQueryChange(it) } - } - - val barModifier = - modifier.fillMaxWidth().let { base -> - scrollBehavior?.let { behavior -> with(behavior) { base.searchBarScrollBehavior() } } ?: base - } - SearchBar( - state = searchBarState, - inputField = { - DocsSearchInputField( - textFieldState = textFieldState, - searchBarState = searchBarState, - placeholder = searchPlaceholder, - clearDescription = clearSearchDescription, - backDescription = backDescription, - testTag = DOCS_SEARCH_BAR_INPUT_FIELD_TAG, - ) - }, - modifier = barModifier, - ) - ExpandedFullScreenSearchBar( - state = searchBarState, - inputField = { - DocsSearchInputField( - textFieldState = textFieldState, - searchBarState = searchBarState, - placeholder = searchPlaceholder, - clearDescription = clearSearchDescription, - backDescription = backDescription, - testTag = DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG, - ) - }, - content = expandedContent, - ) -} - -/** The [SearchBarDefaults.InputField] content shared by the collapsed bar and the expanded overlay. */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun DocsSearchInputField( - textFieldState: TextFieldState, - searchBarState: SearchBarState, - placeholder: String, - clearDescription: String, - backDescription: String, - testTag: String, -) { - val scope = rememberCoroutineScope() - val isExpanded = searchBarState.currentValue == SearchBarValue.Expanded - SearchBarDefaults.InputField( - textFieldState = textFieldState, - searchBarState = searchBarState, - onSearch = { scope.launch { searchBarState.animateToCollapsed() } }, - placeholder = { Text(placeholder) }, - leadingIcon = { - if (isExpanded) { - IconButton(onClick = { scope.launch { searchBarState.animateToCollapsed() } }) { - Icon(imageVector = MeshtasticIcons.ArrowBack, contentDescription = backDescription) - } - } else { - Icon(imageVector = MeshtasticIcons.Search, contentDescription = null) - } - }, - trailingIcon = { - if (textFieldState.text.isNotEmpty()) { - IconButton(onClick = { textFieldState.clearText() }) { - Icon(imageVector = MeshtasticIcons.Close, contentDescription = clearDescription) - } - } - }, - modifier = Modifier.testTag(testTag).semantics { contentDescription = placeholder }, + MeshtasticSearchBar( + query = query, + onQueryChange = onQueryChange, + placeholder = stringResource(Res.string.doc_search_placeholder), + modifier = modifier, + clearDescription = stringResource(Res.string.doc_clear_search), + inputFieldTag = DOCS_SEARCH_BAR_INPUT_FIELD_TAG, + scrollBehavior = scrollBehavior, + expandedContent = expandedContent, ) } diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt index 980545baa4..a95ff456fd 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt @@ -96,7 +96,7 @@ class TranslationCascadeTest { assertIs(success) assertIs(download) assertIs(unavailable) - assertEquals("es", (download as TranslationResult.ModelDownloadRequired).locale) + assertEquals("es", download.locale) } } diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt index 649eaba61d..2192e5d1db 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt @@ -22,6 +22,9 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.navigate_back import org.meshtastic.feature.docs.model.DocPage import org.meshtastic.feature.docs.model.DocSection import kotlin.test.Test @@ -132,7 +135,7 @@ class DocsBrowserScreenTest { onBack = { backCalled = true }, ) } - onNodeWithContentDescription("Navigate back").performClick() + onNodeWithContentDescription(getString(Res.string.navigate_back)).performClick() runOnIdle { assertTrue(backCalled) } } diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreenTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreenTest.kt index c3cae964aa..c5dec18fcb 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreenTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsPageRouteScreenTest.kt @@ -22,6 +22,9 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.navigate_back import org.meshtastic.feature.docs.model.DocPage import org.meshtastic.feature.docs.model.DocPageContent import org.meshtastic.feature.docs.model.DocSection @@ -96,7 +99,7 @@ class DocsPageRouteScreenTest { onBack = { backCalled = true }, ) } - onNodeWithContentDescription("Navigate back").performClick() + onNodeWithContentDescription(getString(Res.string.navigate_back)).performClick() runOnIdle { assertTrue(backCalled) } } diff --git a/feature/firmware/README.md b/feature/firmware/README.md index 4cedc772cd..df8d3c11a8 100644 --- a/feature/firmware/README.md +++ b/feature/firmware/README.md @@ -89,6 +89,8 @@ Two runtime facts make the sketch path safety-critical, not just another UF2 wri - **The nRF52 erase sketch is SoftDevice-version-specific.** Writing the S140 6.1.1 image to a 7.3.0 device (or vice versa) corrupts the SoftDevice with no on-device recovery. `MaintenanceUf2.kt` treats the mounted volume's own `INFO_UF2.TXT` `SoftDevice:` line as authoritative over the bundled hardware-catalog hint — the two must agree, or the app refuses rather than guessing (`EraseImageResolution.Conflict`). The sketch also blocks in `while (!Serial)` until a host asserts DTR, which is what `UsbPassWriter`'s CDC unblock step is for (`MaintenanceUf2.requiresCdcUnblock`). - **OTAFIX bootloaders are resolved by `Board-ID`, not by build target or USB VID/PID** — both of the latter collide across multiple boards. `otafixUf2ForBoardId()` looks up the exact bootloader image for the `Board-ID:` line the volume reports; the Meshtastic build-target name is only ever used to decide whether to *offer* the action in the UI. +A bootloader upgrade stops at `FirmwareUpdateState.ReviewingBootloader` once the drive is read and before anything is downloaded: `parseUf2BootloaderVersion()` takes the installed version from the `UF2 Bootloader` line and `reviewBootloader()` sets it beside the manifest's `otafixReleaseTag`. An exact match reads as up to date and skips the bootloader write; anything else is only "different", since bench and vendor tags carry no order. The running firmware cannot report its bootloader (the app only sees the packed `0x000902` the bootloader leaves in `NRF_TIMER2->CC[0]`, the same for stock and every OTAFIX build), so this is the first point the app knows the installed version. + ```mermaid sequenceDiagram participant App as Android App @@ -123,7 +125,7 @@ A `FirmwareMaintenanceLock` (`:core:common`) is held for the duration of the seq - `SecureDfuTransport.kt`: BLE transport layer for Secure DFU using Kable (control/data point characteristics, PRN flow control). - `DfuZipParser.kt`: Parses Nordic DFU ZIP archives (manifest, init packet, firmware binary). - `UsbUpdateHandler.kt`: Handles USB/UF2 firmware updates across platforms. -- `MaintenanceUf2.kt`: Pinned erase/OTAFIX image resolution, `INFO_UF2.TXT` parsing (Board-ID, SoftDevice, Factory-Erase family), the drive-vs-map SoftDevice resolution used to pick a safe erase sketch, and the bootloader-driven erase resolver that pre-empts it. +- `MaintenanceUf2.kt`: Pinned erase/OTAFIX image resolution, `INFO_UF2.TXT` parsing (bootloader version, Board-ID, SoftDevice, Factory-Erase family), the drive-vs-map SoftDevice resolution used to pick a safe erase sketch, and the bootloader-driven erase resolver that pre-empts it. - `Uf2Header.kt`: UF2 block-header readers (first target address, family ID) that `FirmwareRetriever` checks a downloaded maintenance image against before it can be written. - `UsbMaintenance.kt`: Pure gating (`usbMaintenanceGate`) and volume-inspection/image-choice types for the factory-erase and bootloader-upgrade actions. - `UsbUpdateSupport.kt`: Sequences a maintenance pass (download → reboot to DFU → vet volume → write → confirm landed) and drives the two-pass state machine. @@ -136,16 +138,12 @@ graph TB :feature:firmware[firmware]:::kmp-feature :feature:firmware -.-> :core:ble :feature:firmware -.-> :core:common - :feature:firmware -.-> :core:data - :feature:firmware -.-> :core:database :feature:firmware -.-> :core:datastore :feature:firmware -.-> :core:di :feature:firmware -.-> :core:model :feature:firmware -.-> :core:navigation :feature:firmware -.-> :core:network - :feature:firmware -.-> :core:prefs :feature:firmware -.-> :core:repository - :feature:firmware -.-> :core:service :feature:firmware -.-> :core:resources :feature:firmware -.-> :core:ui :feature:firmware -.-> :core:testing diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index fc0b74e0d4..3c7079d8d9 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -30,22 +30,17 @@ kotlin { commonMain.dependencies { implementation(projects.core.ble) implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) implementation(projects.core.datastore) implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.navigation) implementation(projects.core.network) - implementation(projects.core.prefs) implementation(projects.core.repository) implementation(libs.meshtastic.protobufs) - implementation(projects.core.service) implementation(projects.core.resources) implementation(projects.core.ui) implementation(libs.coil) - implementation(libs.kotlinx.collections.immutable) implementation(libs.ktor.client.core) implementation(libs.ktor.network) implementation(libs.markdown.renderer) @@ -62,6 +57,9 @@ kotlin { // performUsbUpdate resolves compose-resources strings, whose desktop implementation needs // the skiko-awt runtime to read the system theme. - jvmTest.dependencies { implementation(compose.desktop.currentOs) } + jvmTest.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.ktor.client.mock) + } } } diff --git a/feature/firmware/detekt-baseline.xml b/feature/firmware/detekt-baseline.xml index 550a63d6a0..1418f62ef3 100644 --- a/feature/firmware/detekt-baseline.xml +++ b/feature/firmware/detekt-baseline.xml @@ -2,12 +2,13 @@ - Kdoc:LegacyDfuTransport.kt:LegacyDfuTransport$/** * Stream [firmware] to the Packet characteristic, awaiting a [LegacyDfuResponse.PacketReceipt] every * [PRN_INTERVAL_PACKETS] packets and verifying the bytes-received count. * * Watches the connection state in parallel with the write loop; if the link drops mid-stream we cancel the write * coroutine and surface a [DfuException.ConnectionFailed] immediately rather than waiting indefinitely for a write * that will never complete. */ - ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState - ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun DisclaimerDialog - ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState - ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun SuccessState - ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun VerifyingState + AbstractClassCanBeInterface:LegacyDfuProtocol.kt:LegacyDfuResponse$LegacyDfuResponse + AbstractClassCanBeInterface:SecureDfuHandler.kt:BootloaderDetection$BootloaderDetection + AbstractClassCanBeInterface:SecureDfuHandler.kt:DfuUploadResult$DfuUploadResult + AbstractClassCanBeInterface:SecureDfuProtocol.kt:DfuResponse$DfuResponse + AbstractClassCanBeInterface:UnifiedOtaProtocol.kt:OtaCommand$OtaCommand + AbstractClassCanBeInterface:UnifiedOtaProtocol.kt:OtaHandshakeStatus$OtaHandshakeStatus + AbstractClassCanBeInterface:UnifiedOtaProtocol.kt:OtaResponse$OtaResponse MultipleEmitters:FirmwareUpdateScreen.kt:@Composable @Suppress("LongMethod") private fun ReadyState MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState @@ -20,5 +21,9 @@ PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun SuccessStatePreview PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun VerifyingStatePreview TooGenericExceptionCaught:FirmwareUpdateViewModel.kt:FirmwareUpdateViewModel$e: Exception + UnnecessaryLaunchedEffect:FirmwareUpdateScreen.kt:LaunchedEffect + UnusedPrivateProperty:ThroughputTracker.kt:ThroughputTracker$private val timeSource: TimeSource = TimeSource.Monotonic + UseOrEmpty:Esp32OtaUpdateHandler.kt:Esp32OtaUpdateHandler$e.message ?: "" + UseOrEmpty:SecureDfuHandler.kt:SecureDfuHandler$e.message ?: "" diff --git a/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt index 8f3907cc29..eb9c98f16f 100644 --- a/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt @@ -22,25 +22,14 @@ import android.provider.OpenableColumns import co.touchlab.kermit.Logger import com.eygraber.uri.toAndroidUri import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.request.head -import io.ktor.client.statement.bodyAsText -import io.ktor.http.isSuccess import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.model.DeviceHardware import java.io.File -import java.io.FileOutputStream import java.io.IOException import java.io.InputStream -import java.net.URI -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream - -private const val DOWNLOAD_BUFFER_SIZE = 8192 /** SAF provider for physical volumes; the only one a UF2 bootloader drive can appear under. */ private const val EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents" @@ -52,158 +41,12 @@ private const val PRIMARY_VOLUME_ID = "primary" * Helper class to handle file operations related to firmware updates, such as downloading, copying from URI, and * extracting specific files from Zip archives. */ -@Single +@Single(binds = [FirmwareFileHandler::class]) @Suppress("TooManyFunctions") -class AndroidFirmwareFileHandler(private val context: Context, private val client: HttpClient) : FirmwareFileHandler { - private val tempDir = File(context.cacheDir, "firmware_update") +class AndroidFirmwareFileHandler(private val context: Context, client: HttpClient) : + BaseFirmwareFileHandler(client, File(context.cacheDir, "firmware_update")) { - override fun cleanupAllTemporaryFiles() { - runCatching { - if (tempDir.exists()) { - tempDir.deleteRecursively() - } - tempDir.mkdirs() - } - .onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } } - } - - override suspend fun checkUrlExists(url: String): Boolean = withContext(ioDispatcher) { - try { - client.head(url).status.isSuccess() - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Failed to check URL existence: $url" } - false - } - } - - override suspend fun fetchText(url: String): String? = withContext(ioDispatcher) { - try { - val response = client.get(url) - if (response.status.isSuccess()) response.bodyAsText() else null - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Failed to fetch text from: $url" } - null - } - } - - override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): FirmwareArtifact? = - withContext(ioDispatcher) { - val response = - try { - client.get(url) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Download failed for $url" } - return@withContext null - } - - if (!response.status.isSuccess()) { - Logger.w { "Download failed: ${response.status.value} for $url" } - return@withContext null - } - - if (!tempDir.exists()) tempDir.mkdirs() - val targetFile = java.io.File(tempDir, fileName) - downloadResponseToFile(response, targetFile, onProgress) - targetFile.toFirmwareArtifact() - } - - override suspend fun extractFirmwareFromZip( - zipFile: FirmwareArtifact, - hardware: DeviceHardware, - fileExtension: String, - preferredFilename: String?, - ): FirmwareArtifact? = withContext(ioDispatcher) { - val localZipFile = zipFile.toLocalFileOrNull() ?: return@withContext null - val target = hardware.effectiveTarget - if (target.isEmpty() && preferredFilename == null) return@withContext null - - val targetLowerCase = target.lowercase() - val preferredFilenameLower = preferredFilename?.lowercase() - val matchingEntries = mutableListOf>() - - if (!tempDir.exists()) tempDir.mkdirs() - - ZipInputStream(localZipFile.inputStream()).use { zipInput -> - var entry = zipInput.nextEntry - while (entry != null) { - val name = entry.name.lowercase() - // File(name).name strips directory components, mitigating ZipSlip attacks - val entryFileName = File(name).name - - val isMatch = - if (preferredFilenameLower != null) { - entryFileName == preferredFilenameLower - } else { - !entry.isDirectory && isValidFirmwareFile(name, targetLowerCase, fileExtension) - } - - if (isMatch) { - val outFile = File(tempDir, entryFileName) - FileOutputStream(outFile).use { output -> zipInput.copyTo(output) } - matchingEntries.add(entry to outFile) - - if (preferredFilenameLower != null) { - return@withContext outFile.toFirmwareArtifact() - } - } - entry = zipInput.nextEntry - } - } - // Prefer the shortest matching entry name — official release bundles contain one - // matching firmware per target; the heuristic picks the canonical name if multiple match. - matchingEntries.minByOrNull { it.first.name.length }?.second?.toFirmwareArtifact() - } - - override suspend fun extractFirmware( - uri: CommonUri, - hardware: DeviceHardware, - fileExtension: String, - preferredFilename: String?, - ): FirmwareArtifact? = withContext(ioDispatcher) { - val target = hardware.effectiveTarget - if (target.isEmpty() && preferredFilename == null) return@withContext null - - val targetLowerCase = target.lowercase() - val preferredFilenameLower = preferredFilename?.lowercase() - val matchingEntries = mutableListOf>() - - if (!tempDir.exists()) tempDir.mkdirs() - - try { - val platformUri = uri.toAndroidUri() - val inputStream = context.contentResolver.openInputStream(platformUri) ?: return@withContext null - ZipInputStream(inputStream).use { zipInput -> - var entry = zipInput.nextEntry - while (entry != null) { - val name = entry.name.lowercase() - // File(name).name strips directory components, mitigating ZipSlip attacks - val entryFileName = File(name).name - - val isMatch = - if (preferredFilenameLower != null) { - entryFileName == preferredFilenameLower - } else { - !entry.isDirectory && isValidFirmwareFile(name, targetLowerCase, fileExtension) - } - - if (isMatch) { - val outFile = File(tempDir, entryFileName) - FileOutputStream(outFile).use { output -> zipInput.copyTo(output) } - matchingEntries.add(entry to outFile) - - if (preferredFilenameLower != null) { - return@withContext outFile.toFirmwareArtifact() - } - } - entry = zipInput.nextEntry - } - } - } catch (e: IOException) { - Logger.w(e) { "Failed to extract firmware from URI" } - return@withContext null - } - matchingEntries.minByOrNull { it.first.name.length }?.second?.toFirmwareArtifact() - } + override fun openUri(uri: CommonUri): InputStream? = context.contentResolver.openInputStream(uri.toAndroidUri()) override suspend fun getFileSize(file: FirmwareArtifact): Long = withContext(ioDispatcher) { file.toLocalFileOrNull()?.takeIf { it.exists() }?.length() @@ -213,12 +56,6 @@ class AndroidFirmwareFileHandler(private val context: Context, private val clien ?: 0L } - override suspend fun deleteFile(file: FirmwareArtifact) = withContext(ioDispatcher) { - if (!file.isTemporary) return@withContext - val localFile = file.toLocalFileOrNull() ?: return@withContext - if (localFile.exists()) localFile.delete() - } - override suspend fun readBytes(artifact: FirmwareArtifact): ByteArray = withContext(ioDispatcher) { val localFile = artifact.toLocalFileOrNull() if (localFile != null && localFile.exists()) { @@ -230,8 +67,8 @@ class AndroidFirmwareFileHandler(private val context: Context, private val clien } override suspend fun importFromUri(uri: CommonUri): FirmwareArtifact? = withContext(ioDispatcher) { - val inputStream = context.contentResolver.openInputStream(uri.toAndroidUri()) ?: return@withContext null - val tempFile = File(context.cacheDir, "firmware_update/ota_firmware.bin") + val inputStream = openUri(uri) ?: return@withContext null + val tempFile = File(tempDir, "ota_firmware.bin") tempFile.parentFile?.mkdirs() inputStream.use { input -> tempFile.outputStream().use { output -> input.copyTo(output) } } tempFile.toFirmwareArtifact() @@ -293,9 +130,6 @@ class AndroidFirmwareFileHandler(private val context: Context, private val clien ?: throw IOException("Cannot open artifact: ${artifact.uri}") } - private fun isValidFirmwareFile(filename: String, target: String, fileExtension: String): Boolean = - org.meshtastic.feature.firmware.isValidFirmwareFile(filename, target, fileExtension) - /** * Accepts only a Storage Access Framework document on a non-primary external volume. * @@ -387,16 +221,4 @@ class AndroidFirmwareFileHandler(private val context: Context, private val clien inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } } - - private fun File.toFirmwareArtifact(): FirmwareArtifact = - FirmwareArtifact(uri = CommonUri.parse(toURI().toString()), fileName = name, isTemporary = true) - - private fun FirmwareArtifact.toLocalFileOrNull(): File? { - val uriString = uri.toString() - return if (uriString.startsWith("file:")) { - runCatching { File(URI(uriString)) }.getOrNull() - } else { - null - } - } } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt index cdde4a6ece..b2b721c602 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt @@ -20,8 +20,8 @@ import org.koin.core.annotation.Single import org.meshtastic.core.common.state.RadioOperation import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.isBle import org.meshtastic.core.repository.isSerial diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt index 8aa2339df7..ff888d1a86 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt @@ -117,7 +117,8 @@ interface FirmwareFileHandler { * @param hardware Used to match the correct binary inside the zip. * @param fileExtension The extension to filter for (e.g. ".bin", ".uf2"). * @param preferredFilename Optional exact filename to prefer within the zip. - * @return The extracted [FirmwareArtifact], or `null` if no matching file was found. + * @return The extracted [FirmwareArtifact], or `null` if no file matched or the archive is corrupt or over the + * extraction limits. */ suspend fun extractFirmware( uri: CommonUri, @@ -133,7 +134,8 @@ interface FirmwareFileHandler { * @param hardware Used to match the correct binary inside the zip. * @param fileExtension The extension to filter for (e.g. ".bin", ".uf2"). * @param preferredFilename Optional exact filename to prefer within the zip. - * @return The extracted [FirmwareArtifact], or `null` if no matching file was found. + * @return The extracted [FirmwareArtifact], or `null` if no file matched or the archive is corrupt or over the + * extraction limits. */ suspend fun extractFirmwareFromZip( zipFile: FirmwareArtifact, @@ -151,14 +153,16 @@ interface FirmwareFileHandler { /** * Check whether [filename] is a valid firmware binary for [target] with the expected [fileExtension]. Excludes - * non-firmware binaries that share the same extension (e.g. `littlefs-*`, `bleota*`). + * non-firmware binaries that share the same extension (e.g. `littlefs-*`, `bleota*`), wherever they sit in a zip's + * directories. */ @Suppress("ComplexCondition") // excluded-binary + target/extension guards collapsed to one early-out internal fun isValidFirmwareFile(filename: String, target: String, fileExtension: String): Boolean { + val baseName = filename.substringAfterLast('/') if ( - filename.startsWith("littlefs-") || - filename.startsWith("bleota") || - filename.startsWith("mt-") || + baseName.startsWith("littlefs-") || + baseName.startsWith("bleota") || + baseName.startsWith("mt-") || filename.contains(".factory.") || target.isBlank() || !filename.endsWith(fileExtension) diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt index 8eeb31b07a..08a7b76543 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt @@ -92,7 +92,43 @@ internal fun UsbMaintenanceCardPreview() { AppTheme { Surface { Column(modifier = Modifier.padding(24.dp)) { - UsbMaintenanceCard(deviceName = "RAK4631", onBootloaderUpgrade = {}) + UsbMaintenanceCard( + deviceName = "RAK4631", + latestBootloader = "0.9.2-OTAFIX2.5", + onBootloaderUpgrade = {}, + ) + } + } + } +} + +@PreviewLightDark +@Composable +internal fun BootloaderReviewPreview() { + AppTheme { + Surface { + Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + BootloaderReviewState( + versions = BootloaderVersions(installed = "0.9.2-OTAFIX2.3-BP1.5", available = "0.9.2-OTAFIX2.5"), + onUpgrade = {}, + onSkip = {}, + ) + } + } + } +} + +@PreviewLightDark +@Composable +internal fun BootloaderUpToDatePreview() { + AppTheme { + Surface { + Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + BootloaderReviewState( + versions = BootloaderVersions(installed = "0.9.2-OTAFIX2.5", available = "0.9.2-OTAFIX2.5"), + onUpgrade = {}, + onSkip = {}, + ) } } } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt index 5c7d6265df..217edad26c 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt @@ -17,12 +17,13 @@ package org.meshtastic.feature.firmware import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import org.koin.core.annotation.Single -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.core.network.HttpClientDefaults import org.meshtastic.feature.firmware.ota.FirmwareHashUtil @@ -99,6 +100,8 @@ open class FirmwareRetriever(private val fileHandler: FirmwareFileHandler) { val artifact = try { fileHandler.downloadFile(asset.url, asset.fileName, onProgress) + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.w(e) { "Maintenance image download failed: ${asset.fileName}" } null @@ -305,6 +308,8 @@ open class FirmwareRetriever(private val fileHandler: FirmwareFileHandler) { fileHandler.downloadFile(directUrl, filename, onProgress)?.let { return it } + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.w(e) { "Direct download for $filename failed, falling back to release zip" } } @@ -319,6 +324,8 @@ open class FirmwareRetriever(private val fileHandler: FirmwareFileHandler) { val zipUrl = resolveZipUrl(release.zipUrl, hardware.architecture) try { fileHandler.downloadFile(zipUrl, "firmware_release.zip", onProgress) + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.w(e) { "Release zip download failed for ${release.id}" } null diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt index a67f85a402..5d31a24c34 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.feature.firmware -import org.meshtastic.core.database.entity.FirmwareReleaseType +import org.meshtastic.core.model.FirmwareReleaseType data class FirmwareUpdateActions( val onReleaseTypeSelect: (FirmwareReleaseType) -> Unit, @@ -27,6 +27,9 @@ data class FirmwareUpdateActions( /** Pick the device's UF2 volume for a maintenance pass, which vets the drive before writing to it. */ val onPickVolume: () -> Unit, val onBootloaderUpgrade: () -> Unit, + val onConfirmBootloaderUpgrade: () -> Unit, + /** Leaves the bootloader as it is and moves on to reinstalling the firmware. */ + val onSkipBootloaderUpgrade: () -> Unit, val onConfirmLocalFile: () -> Unit, val onDismissLocalFile: () -> Unit, val onRetry: () -> Unit, diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt index 3cb9a3dfbd..a34ed7060a 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.firmware import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease /** Common interface for all firmware update handlers (BLE DFU, ESP32 OTA, USB). */ interface FirmwareUpdateHandler { diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt index 5c6cd9ef51..c8afdf54cc 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.firmware import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease /** * Routes firmware update requests to the appropriate platform-specific handler based on the active connection type diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt index 05ca499c7f..b87acc69b9 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt @@ -85,15 +85,20 @@ import kotlinx.coroutines.delay import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.back import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.chirpy import org.meshtastic.core.resources.dont_show_again_for_device +import org.meshtastic.core.resources.firmware_maintenance_bootloader_available +import org.meshtastic.core.resources.firmware_maintenance_bootloader_installed +import org.meshtastic.core.resources.firmware_maintenance_bootloader_latest_hint +import org.meshtastic.core.resources.firmware_maintenance_bootloader_up_to_date +import org.meshtastic.core.resources.firmware_maintenance_bootloader_up_to_date_hint import org.meshtastic.core.resources.firmware_maintenance_select_drive import org.meshtastic.core.resources.firmware_maintenance_upgrade_bootloader_action import org.meshtastic.core.resources.firmware_maintenance_upgrade_confirm_text @@ -150,8 +155,11 @@ import org.meshtastic.core.resources.i_know_what_i_m_doing import org.meshtastic.core.resources.img_chirpy import org.meshtastic.core.resources.img_hw_unknown import org.meshtastic.core.resources.learn_more +import org.meshtastic.core.resources.next import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.save +import org.meshtastic.core.resources.skip +import org.meshtastic.core.resources.unknown import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.Bluetooth @@ -218,6 +226,8 @@ fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateView onSaveFile = { fileName -> saveFileLauncher(fileName, UF2_MIME_TYPE) }, onPickVolume = volumePickerLauncher, onBootloaderUpgrade = viewModel::startBootloaderUpgrade, + onConfirmBootloaderUpgrade = viewModel::confirmBootloaderUpgrade, + onSkipBootloaderUpgrade = viewModel::skipBootloaderUpgrade, onConfirmLocalFile = viewModel::confirmLocalFirmwareFile, onDismissLocalFile = viewModel::dismissLocalFirmwareFile, onRetry = viewModel::checkForUpdates, @@ -394,6 +404,9 @@ private fun shouldKeepFirmwareScreenOn(state: FirmwareUpdateState): Boolean = wh // ViewModel, so letting the screen sleep (and the ViewModel clear) would strand the device. is FirmwareUpdateState.AwaitingFileSave -> state.step.isDestructive || state.retryMessage != null + // The device is sitting in update mode with the rest of the sequence queued in the ViewModel. + is FirmwareUpdateState.ReviewingBootloader -> true + else -> false } @@ -438,6 +451,13 @@ private fun FirmwareUpdateContent( deviceWasWiped = state.deviceWasWiped, ) + is FirmwareUpdateState.ReviewingBootloader -> + BootloaderReviewState( + versions = state.versions, + onUpgrade = actions.onConfirmBootloaderUpgrade, + onSkip = actions.onSkipBootloaderUpgrade, + ) + is FirmwareUpdateState.AwaitingFileSave -> AwaitingFileSaveState( state = state, @@ -515,7 +535,11 @@ private fun ReadyState( } if (state.maintenance.showBootloaderUpgrade) { - UsbMaintenanceCard(deviceName = device.displayName, onBootloaderUpgrade = actions.onBootloaderUpgrade) + UsbMaintenanceCard( + deviceName = device.displayName, + latestBootloader = state.maintenance.latestBootloader, + onBootloaderUpgrade = actions.onBootloaderUpgrade, + ) Spacer(Modifier.height(16.dp)) } @@ -884,7 +908,7 @@ private fun DeviceInfoCard( * on. */ @Composable -internal fun UsbMaintenanceCard(deviceName: String, onBootloaderUpgrade: () -> Unit) { +internal fun UsbMaintenanceCard(deviceName: String, latestBootloader: String?, onBootloaderUpgrade: () -> Unit) { var showUpgradeConfirmation by rememberSaveable { mutableStateOf(false) } if (showUpgradeConfirmation) { @@ -906,6 +930,76 @@ internal fun UsbMaintenanceCard(deviceName: String, onBootloaderUpgrade: () -> U TextButton(onClick = { showUpgradeConfirmation = true }) { Text(stringResource(Res.string.firmware_maintenance_upgrade_bootloader_action)) } + latestBootloader?.let { + Text( + text = stringResource(Res.string.firmware_maintenance_bootloader_latest_hint, it), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp), + ) + } + } + } +} + +/** + * Installed against latest bootloader, read from the drive before an upgrade is written. The running firmware cannot + * report its bootloader, so this is the first point the app knows the installed version. + */ +@Composable +internal fun BootloaderReviewState(versions: BootloaderVersions, onUpgrade: () -> Unit, onSkip: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + if (versions.isCurrent) MeshtasticIcons.CheckCircle else MeshtasticIcons.Usb, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.height(24.dp)) + Text( + stringResource( + if (versions.isCurrent) { + Res.string.firmware_maintenance_bootloader_up_to_date + } else { + Res.string.firmware_maintenance_upgrade_confirm_title + }, + ), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(16.dp)) + Text( + stringResource( + Res.string.firmware_maintenance_bootloader_installed, + versions.installed ?: stringResource(Res.string.unknown), + ), + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace, + textAlign = TextAlign.Center, + ) + Text( + stringResource(Res.string.firmware_maintenance_bootloader_available, versions.available), + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(24.dp)) + if (versions.isCurrent) { + Text( + stringResource(Res.string.firmware_maintenance_bootloader_up_to_date_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(16.dp)) + Button(onClick = onSkip) { Text(stringResource(Res.string.next)) } + } else { + Row(horizontalArrangement = spacedBy(16.dp)) { + OutlinedButton(onClick = onSkip) { Text(stringResource(Res.string.skip)) } + Button(onClick = onUpgrade) { + Text(stringResource(Res.string.firmware_maintenance_upgrade_bootloader_action)) + } + } } } } @@ -1034,7 +1128,7 @@ private fun ProgressContent( if (details != null) { Spacer(Modifier.height(4.dp)) Text( - text = details, + text = details.asString(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt index 39a2ae47e4..6408ecc596 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt @@ -16,22 +16,26 @@ */ package org.meshtastic.feature.firmware -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.repository.FirmwareUpdateProgress +import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_verifying +import kotlin.math.roundToInt /** * Represents the progress of a long-running firmware update task. * * @property message A high-level status message (e.g., "Downloading..."). * @property progress A value between 0.0 and 1.0 representing completion percentage. - * @property details Optional high-frequency detail text (e.g., "1.2 MiB/s, 45%"). + * @property details Optional high-frequency detail text (e.g., "45% (12.60 kB/s, ETA: 5s)"). * @property hint Optional persistent advisory shown alongside the progress (e.g. a slow-bootloader tip). */ data class ProgressState( val message: UiText = UiText.DynamicString(""), val progress: Float = 0f, - val details: String? = null, + val details: UiText? = null, val hint: UiText? = null, ) @@ -67,8 +71,14 @@ sealed interface FirmwareUpdateState { /** Firmware file is being downloaded from the release server. */ data class Downloading(val progressState: ProgressState) : FirmwareUpdateState - /** Intermediate processing (e.g. extracting, preparing DFU). */ - data class Processing(val progressState: ProgressState) : FirmwareUpdateState + /** + * Intermediate processing (e.g. extracting, preparing DFU). + * + * @property beforeConfirmation True while a picked local file is checked before the user has confirmed any update, + * so nothing outside the screen reports it as a running update. + */ + data class Processing(val progressState: ProgressState, val beforeConfirmation: Boolean = false) : + FirmwareUpdateState /** Firmware is actively being written to the device. */ data class Updating(val progressState: ProgressState) : FirmwareUpdateState @@ -110,8 +120,44 @@ sealed interface FirmwareUpdateState { val step: UsbFileSaveStep = UsbFileSaveStep.Firmware, val retryMessage: UiText? = null, ) : FirmwareUpdateState + + /** + * The device's update drive has been read for a bootloader upgrade and nothing has been written yet. The user + * either upgrades or skips straight to reinstalling the firmware, which is also what restarts the device. + */ + data class ReviewingBootloader(val versions: BootloaderVersions) : FirmwareUpdateState } +/** + * The part of this state the foreground-service notification shows, or null when nothing is transferring. Determinate + * exactly where the screen draws a determinate bar: downloading and writing, not the processing and verifying waits. + */ +internal fun FirmwareUpdateState.toUpdateProgress(): FirmwareUpdateProgress? = when (this) { + is FirmwareUpdateState.Downloading -> FirmwareUpdateProgress(progressState.message, progressState.percent()) + + is FirmwareUpdateState.Updating -> FirmwareUpdateProgress(progressState.message, progressState.percent()) + + is FirmwareUpdateState.Processing -> + if (beforeConfirmation) null else FirmwareUpdateProgress(progressState.message, percent = null) + + FirmwareUpdateState.Verifying -> + FirmwareUpdateProgress(UiText.Resource(Res.string.firmware_update_verifying), percent = null) + + FirmwareUpdateState.Idle, + FirmwareUpdateState.Checking, + is FirmwareUpdateState.Ready, + FirmwareUpdateState.VerificationFailed, + is FirmwareUpdateState.Error, + is FirmwareUpdateState.Success, + is FirmwareUpdateState.AwaitingFileSave, + is FirmwareUpdateState.ReviewingBootloader, + -> null +} + +private const val PERCENT = 100 + +private fun ProgressState.percent(): Int = (progress * PERCENT).roundToInt().coerceIn(0, PERCENT) + private val FORMAT_ARG_REGEX = Regex(":?\\s*%1\\\$d%?") /** Strip positional format arguments (e.g. `%1$d`) from a localized template to get a clean base message. */ diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index db49f8121c..4e28dd8216 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -30,14 +30,17 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.common.di.ApplicationCoroutineScope import org.meshtastic.core.common.state.HiddenFeaturesUnlock import org.meshtastic.core.common.state.OperationLease @@ -45,18 +48,20 @@ import org.meshtastic.core.common.state.RadioOperation import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.datastore.FirmwareRecoveryDataSource import org.meshtastic.core.datastore.model.PendingFirmwareRecovery import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceAddress import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FirmwareReleaseRepository +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.MaintenanceUf2Repository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NodeRestartTracker @@ -66,6 +71,7 @@ import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.isBle import org.meshtastic.core.repository.isSerial import org.meshtastic.core.repository.isTcp +import org.meshtastic.core.repository.selectedDevice import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.firmware_maintenance_cdc_unblock_failed @@ -132,6 +138,8 @@ class FirmwareUpdateViewModel( private val hiddenFeaturesUnlock: HiddenFeaturesUnlock, private val analytics: PlatformAnalytics, private val nodeRestartTracker: NodeRestartTracker, + private val bluetoothRepository: BluetoothRepository, + private val firmwareUpdateStatusRepository: FirmwareUpdateStatusRepository, ) : ViewModel() { /** The USB maintenance sequence's hold on the radio. Spans several passes, so it cannot use `withOperation`. */ @@ -183,6 +191,9 @@ class FirmwareUpdateViewModel( */ private var maintenanceWriteJob: Job? = null + /** The drive read for [FirmwareUpdateState.ReviewingBootloader], written to if the user confirms the upgrade. */ + private var reviewedVolume: CommonUri? = null + /** * True once an erase or bootloader image has been written, which is the point the device stops having a working * application. From then on failures re-offer the pass instead of surfacing a dead end. @@ -202,6 +213,14 @@ class FirmwareUpdateViewModel( tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) checkForUpdates() } + // One observer of every state write, so the foreground-service notification can follow a flash the user has + // backgrounded without any write site having to remember to publish. + viewModelScope.launch { + _state + .map { it.toUpdateProgress() } + .distinctUntilChanged() + .collect(firmwareUpdateStatusRepository::publishProgress) + } } @OptIn(DelicateCoroutinesApi::class) @@ -214,6 +233,7 @@ class FirmwareUpdateViewModel( // leaked lock would permanently suppress the radio transport's auto-reconnect for the rest of the app // session — see startUsbMaintenance/advancePastPass. A no-op if no sequence was in flight. releaseMaintenanceLease() + firmwareUpdateStatusRepository.publishProgress(null) // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the // application-wide scope (SupervisorJob + ioDispatcher). ATOMIC start + NonCancellable // context keeps cleanup running even if something tries to cancel it mid-flight. @@ -258,6 +278,7 @@ class FirmwareUpdateViewModel( private fun endMaintenanceSequence() { maintenanceWriteJob = null pendingUsbPasses = emptyList() + reviewedVolume = null destructiveWriteDone = false maintenanceHardware = null releaseMaintenanceLease() @@ -267,92 +288,96 @@ class FirmwareUpdateViewModel( fun checkForUpdates() { updateJob?.cancel() clearPendingLocalFirmwareFile() - updateJob = - viewModelScope.launch { - _state.value = FirmwareUpdateState.Checking - safeCatching { - val ourNode = nodeRepository.myNodeInfo.value - val address = radioPrefs.devAddr.value?.drop(1) - if (address == null || ourNode == null) { - // Not connected: offer to re-flash a device stranded in bootloader mode if we saved a - // recovery record when its (now-interrupted) update was triggered. Otherwise, no device. - enterRecoveryModeOrError() - return@launch - } - val deviceHardware = getDeviceHardware(ourNode) ?: return@launch - _deviceHardware.value = deviceHardware - _currentFirmwareVersion.value = ourNode.firmwareVersion - // Best-effort, once per check — not per release-flow emission below. A failed fetch, or one - // carrying no images at all, leaves the cache/seed untouched, so this never regresses the - // maintenance gate. - maintenanceUf2Repository.reconcile() - val maintenanceUf2Manifest = maintenanceUf2Repository.getSnapshot() - - val releaseFlow = - if (_selectedReleaseType.value == FirmwareReleaseType.LOCAL) { - flowOf(null) - } else { - firmwareReleaseRepository.getReleaseFlow(_selectedReleaseType.value) - } - releaseFlow.collectLatest { release -> - _selectedRelease.value = release - - val dismissed = bootloaderWarningDataSource.isDismissed(address) - val firmwareUpdateMethod = - when { - radioPrefs.isSerial() -> { - // Serial OTA is not yet supported for ESP32 — only nRF52/RP2040 UF2. - if (deviceHardware.isEsp32Arc) { - FirmwareUpdateMethod.Unknown - } else { - FirmwareUpdateMethod.Usb - } - } - - radioPrefs.isBle() -> FirmwareUpdateMethod.Ble - - radioPrefs.isTcp() -> { - // WiFi OTA is ESP32-only; nRF52/RP2040 have no TCP update path. - if (deviceHardware.isEsp32Arc) { - FirmwareUpdateMethod.Wifi - } else { - FirmwareUpdateMethod.Unknown - } - } - - else -> FirmwareUpdateMethod.Unknown - } - _state.value = - FirmwareUpdateState.Ready( - release = release, - deviceHardware = deviceHardware, - address = address, - showBootloaderWarning = - deviceHardware.requiresBootloaderUpgradeForOta == true && - !dismissed && - radioPrefs.isBle(), - updateMethod = firmwareUpdateMethod, - currentFirmwareVersion = ourNode.firmwareVersion, - maintenance = - usbMaintenanceGate( - manifest = maintenanceUf2Manifest, - hardware = deviceHardware, - updateMethod = firmwareUpdateMethod, - hasRelease = release != null, - platformSupportsMaintenance = usbManager.supportsUf2Maintenance, - ), - ) - } + updateJob = viewModelScope.launch { + _state.value = FirmwareUpdateState.Checking + safeCatching { + val ourNode = nodeRepository.myNodeInfo.value + val address = radioPrefs.selectedDevice?.identity + if (address == null || ourNode == null) { + // Not connected: offer to re-flash a device stranded in bootloader mode if we saved a + // recovery record when its (now-interrupted) update was triggered. Otherwise, no device. + enterRecoveryModeOrError() + return@launch } - .onFailure { e -> - Logger.e(e) { "Error checking for updates" } - val unknownError = UiText.Resource(Res.string.firmware_update_unknown_error) - _state.value = - FirmwareUpdateState.Error( - if (e.message != null) UiText.DynamicString(e.message!!) else unknownError, - ) + val deviceHardware = getDeviceHardware(ourNode) ?: return@launch + _deviceHardware.value = deviceHardware + _currentFirmwareVersion.value = ourNode.firmwareVersion + // Best-effort, once per check — not per release-flow emission below. A failed fetch, or one + // carrying no images at all, leaves the cache/seed untouched, so this never regresses the + // maintenance gate. + maintenanceUf2Repository.reconcile() + val maintenanceUf2Manifest = maintenanceUf2Repository.getSnapshot() + + val releaseFlow = + if (_selectedReleaseType.value == FirmwareReleaseType.LOCAL) { + flowOf(null) + } else { + firmwareReleaseRepository.getReleaseFlow(_selectedReleaseType.value) } + releaseFlow.collectLatest { release -> + _selectedRelease.value = release + + val dismissed = bootloaderWarningDataSource.isDismissed(address) + val firmwareUpdateMethod = + when { + radioPrefs.isSerial() -> { + // Serial OTA is not yet supported for ESP32 — only nRF52/RP2040 UF2. + if (deviceHardware.isEsp32Arc) { + FirmwareUpdateMethod.Unknown + } else { + FirmwareUpdateMethod.Usb + } + } + + // A saved BLE address restored onto hardware with no Bluetooth LE has no BLE path. + radioPrefs.isBle() -> { + if (bluetoothRepository.isSupported) { + FirmwareUpdateMethod.Ble + } else { + FirmwareUpdateMethod.Unknown + } + } + + radioPrefs.isTcp() -> { + // WiFi OTA is ESP32-only; nRF52/RP2040 have no TCP update path. + if (deviceHardware.isEsp32Arc) { + FirmwareUpdateMethod.Wifi + } else { + FirmwareUpdateMethod.Unknown + } + } + + else -> FirmwareUpdateMethod.Unknown + } + _state.value = + FirmwareUpdateState.Ready( + release = release, + deviceHardware = deviceHardware, + address = address, + showBootloaderWarning = + deviceHardware.requiresBootloaderUpgradeForOta == true && + !dismissed && + radioPrefs.isBle(), + updateMethod = firmwareUpdateMethod, + currentFirmwareVersion = ourNode.firmwareVersion, + maintenance = + usbMaintenanceGate( + manifest = maintenanceUf2Manifest, + hardware = deviceHardware, + updateMethod = firmwareUpdateMethod, + hasRelease = release != null, + platformSupportsMaintenance = usbManager.supportsUf2Maintenance, + ), + ) + } } + .onFailure { e -> + Logger.e(e) { "Error checking for updates" } + val unknownError = UiText.Resource(Res.string.firmware_update_unknown_error) + _state.value = + FirmwareUpdateState.Error(e.message?.let { UiText.DynamicString(it) } ?: unknownError) + } + } } /** @@ -361,7 +386,8 @@ class FirmwareUpdateViewModel( * first reconnecting (the bootloader exposes no mesh service to connect to). No record ⇒ the usual "no device". */ private suspend fun enterRecoveryModeOrError() { - val recovery = firmwareRecoveryDataSource.pending.first() + // Recovery re-flashes over BLE only; without Bluetooth LE the record is kept but not offered. + val recovery = firmwareRecoveryDataSource.pending.first()?.takeIf { bluetoothRepository.isSupported } if (recovery == null) { clearDeviceMetadata() _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_no_device)) @@ -380,8 +406,10 @@ class FirmwareUpdateViewModel( _deviceHardware.value = hardware _currentFirmwareVersion.value = null - val type = - runCatching { FirmwareReleaseType.valueOf(recovery.releaseType) }.getOrDefault(FirmwareReleaseType.STABLE) + val type = runCatching { + FirmwareReleaseType.valueOf(recovery.releaseType) + } + .getOrDefault(FirmwareReleaseType.STABLE) // A nightly recovery record can only exist if the user had unlocked the hidden channel and deliberately // flashed nightly before the interruption; re-assert the (process-scoped) unlock so the recovery UI can // show and re-fetch that channel instead of leaving the stranded device unrecoverable. @@ -453,57 +481,57 @@ class FirmwareUpdateViewModel( } } + @Suppress("SuspendFunSwallowedCancellation") // cancellation resets the UI state, then is rethrown private fun startNormalUpdate(currentState: FirmwareUpdateState.Ready, release: FirmwareRelease) { originalDeviceAddress = radioPrefs.devAddr.value viewModelScope.launch { if (checkBatteryLevel()) { updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - // Persist a recovery record before flashing so a stranded bootloader (interrupted upload, - // app closed, missed reconnect) can be re-flashed later while disconnected. - maybeRecordRecovery(currentState) - tempFirmwareFile = - firmwareUpdateManager.startUpdate( - release = release, - hardware = currentState.deviceHardware, - address = currentState.address, - updateState = { _state.value = it }, - ) + updateJob = viewModelScope.launch { + try { + // Persist a recovery record before flashing so a stranded bootloader (interrupted upload, + // app closed, missed reconnect) can be re-flashed later while disconnected. + maybeRecordRecovery(currentState) + tempFirmwareFile = + firmwareUpdateManager.startUpdate( + release = release, + hardware = currentState.deviceHardware, + address = currentState.address, + updateState = { _state.value = it }, + ) - when (val finalState = _state.value) { - is FirmwareUpdateState.Success -> - verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) + when (val finalState = _state.value) { + is FirmwareUpdateState.Success -> + verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) - // USB/UF2 path intentionally pauses here: the UI launches the file picker and - // saveDfuFile() resumes the flow. Leave the state intact (tempFirmwareFile holds - // the artifact for cleanup after the copy completes). - is FirmwareUpdateState.AwaitingFileSave -> Unit + // USB/UF2 path intentionally pauses here: the UI launches the file picker and + // saveDfuFile() resumes the flow. Leave the state intact (tempFirmwareFile holds + // the artifact for cleanup after the copy completes). + is FirmwareUpdateState.AwaitingFileSave -> Unit - is FirmwareUpdateState.Error -> { - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } - - else -> { - // Defense-in-depth: handler returned without setting a terminal state - Logger.w { "Firmware update returned without terminal state: ${_state.value}" } - _state.value = - FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } + is FirmwareUpdateState.Error -> { + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + } + + else -> { + // Defense-in-depth: handler returned without setting a terminal state + Logger.w { "Firmware update returned without terminal state: ${_state.value}" } + _state.value = + FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) } - } catch (e: CancellationException) { - Logger.w(e) { "Firmware update cancelled — cause: ${e.cause} message: ${e.message}" } - _state.value = FirmwareUpdateState.Idle - checkForUpdates() - throw e - } catch (e: Exception) { - Logger.e(e) { "Firmware update failed" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) } + } catch (e: CancellationException) { + Logger.w(e) { "Firmware update cancelled — cause: ${e.cause} message: ${e.message}" } + _state.value = FirmwareUpdateState.Idle + checkForUpdates() + throw e + } catch (e: Exception) { + Logger.e(e) { "Firmware update failed" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) } + } } } } @@ -539,50 +567,50 @@ class FirmwareUpdateViewModel( * Re-flash a device stranded in bootloader mode. Routes straight to BLE DFU (the device is disconnected, so the * connection-type dispatch can't run) and reuses the same verify/cleanup tail as a normal update. */ + @Suppress("SuspendFunSwallowedCancellation") // cancellation resets the UI state, then is rethrown private fun startRecoveryUpdate(currentState: FirmwareUpdateState.Ready, release: FirmwareRelease) { originalDeviceAddress = pendingRecovery?.fullAddress updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - tempFirmwareFile = - firmwareUpdateManager.recoverDfuDevice( - release = release, - hardware = currentState.deviceHardware, - address = currentState.address, - updateState = { _state.value = it }, - ) + updateJob = viewModelScope.launch { + try { + tempFirmwareFile = + firmwareUpdateManager.recoverDfuDevice( + release = release, + hardware = currentState.deviceHardware, + address = currentState.address, + updateState = { _state.value = it }, + ) - when (val finalState = _state.value) { - is FirmwareUpdateState.Success -> - verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) + when (val finalState = _state.value) { + is FirmwareUpdateState.Success -> + verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) - is FirmwareUpdateState.Error -> { - // BLE re-flash of a stranded device failed. A stock nRF bootloader can't reliably finish - // an interrupted OTA update over the air, so point the user at USB serial-DFU recovery - // rather than surfacing the low-level connection error. - _state.value = - FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } - - else -> { - Logger.w { "Firmware recovery returned without terminal state: ${_state.value}" } - _state.value = - FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } + is FirmwareUpdateState.Error -> { + // BLE re-flash of a stranded device failed. A stock nRF bootloader can't reliably finish + // an interrupted OTA update over the air, so point the user at USB serial-DFU recovery + // rather than surfacing the low-level connection error. + _state.value = + FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + } + + else -> { + Logger.w { "Firmware recovery returned without terminal state: ${_state.value}" } + _state.value = + FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) } - } catch (e: CancellationException) { - Logger.w(e) { "Firmware recovery cancelled" } - _state.value = FirmwareUpdateState.Idle - checkForUpdates() - throw e - } catch (e: Exception) { - Logger.e(e) { "Firmware recovery failed" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) } + } catch (e: CancellationException) { + Logger.w(e) { "Firmware recovery cancelled" } + _state.value = FirmwareUpdateState.Idle + checkForUpdates() + throw e + } catch (e: Exception) { + Logger.e(e) { "Firmware recovery failed" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_recovery_ble_failed)) } + } } // ── USB maintenance (factory erase / bootloader upgrade) ──────────────────────────────────────── @@ -634,32 +662,31 @@ class FirmwareUpdateViewModel( // the radio transport mid-sequence and bind it to the erase firmware's bare CDC port. maintenanceLease = radioOperationLock.acquire(RadioOperation.FirmwareMaintenance) updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - pendingUsbPasses = - performUsbMaintenance( - request = request, - release = release, - hardware = currentState.deviceHardware, - radioController = radioController, - nodeRepository = nodeRepository, - updateState = { _state.value = it }, - retrieveUsbFirmware = firmwareRetriever::retrieveUsbFirmware, - ) - // The firmware image is the last pass, so it is also what must be cleaned up if the flow dies. - tempFirmwareFile = - pendingUsbPasses.filterIsInstance().lastOrNull()?.artifact - } catch (e: CancellationException) { - throw e - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.e(e) { "USB maintenance preparation failed" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - } finally { - // Preparation that produced no passes never reached the device; hand the transport back. - if (pendingUsbPasses.isEmpty()) releaseMaintenanceLease() - } + updateJob = viewModelScope.launch { + try { + pendingUsbPasses = + performUsbMaintenance( + request = request, + release = release, + hardware = currentState.deviceHardware, + radioController = radioController, + nodeRepository = nodeRepository, + updateState = { _state.value = it }, + retrieveUsbFirmware = firmwareRetriever::retrieveUsbFirmware, + ) + // The firmware image is the last pass, so it is also what must be cleaned up if the flow dies. + tempFirmwareFile = + pendingUsbPasses.filterIsInstance().lastOrNull()?.artifact + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.e(e) { "USB maintenance preparation failed" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) + } finally { + // Preparation that produced no passes never reached the device; hand the transport back. + if (pendingUsbPasses.isEmpty()) releaseMaintenanceLease() } + } } } @@ -677,25 +704,74 @@ class FirmwareUpdateViewModel( if (pass.step != currentState.step) return val hardware = maintenanceHardware ?: return - maintenanceWriteJob = - viewModelScope.launch { - try { - // Capture the ports present before the write so the erase image's port can be told from - // pre-existing - // ones. - val portsBefore = usbManager.serialPortKeys() - val result = usbPassWriter(portsBefore).write(pass, treeUri, hardware) { _state.value = it } - handlePassResult(pass, result) - } catch (e: CancellationException) { - // The write has stopped, so the device can be handed back. Doing this here rather than in - // cancelUpdate() is the point: the sequence keeps the radio until the write actually ends. - endMaintenanceSequence() - throw e - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.e(e) { "Writing ${pass.step} failed" } - reofferOrFail(pass, UiText.Resource(Res.string.firmware_update_failed)) + if (pass.step == UsbFileSaveStep.BootloaderUpgrade) { + reviewBootloaderPass(pass, treeUri) + } else { + launchPassWrite(pass, treeUri, hardware) + } + } + + /** Writes the reviewed bootloader upgrade to the drive the review read. */ + @Suppress("ReturnCount") // preconditions guarding a destructive write + fun confirmBootloaderUpgrade() { + if (_state.value !is FirmwareUpdateState.ReviewingBootloader) return + val pass = pendingUsbPasses.firstOrNull()?.takeIf { it.step == UsbFileSaveStep.BootloaderUpgrade } ?: return + val treeUri = reviewedVolume ?: return + val hardware = maintenanceHardware ?: return + reviewedVolume = null + launchPassWrite(pass, treeUri, hardware) + } + + /** Moves on to reinstalling the firmware without writing the bootloader, so nothing destructive has happened. */ + fun skipBootloaderUpgrade() { + if (_state.value !is FirmwareUpdateState.ReviewingBootloader) return + val pass = pendingUsbPasses.firstOrNull()?.takeIf { it.step == UsbFileSaveStep.BootloaderUpgrade } ?: return + reviewedVolume = null + viewModelScope.launch { advancePastPass(pass, written = false) } + } + + @Suppress("SuspendFunSwallowedCancellation") // cancellation hands the device back, then is rethrown + private fun reviewBootloaderPass(pass: UsbFileSavePass, treeUri: CommonUri) { + maintenanceWriteJob = viewModelScope.launch { + try { + when (val review = usbPassWriter(portsBefore = emptySet()).review(treeUri)) { + is BootloaderReview.Refused -> reofferOrFail(pass, usbMaintenanceRefusalMessage(review.reason)) + + is BootloaderReview.Ready -> { + reviewedVolume = treeUri + _state.value = FirmwareUpdateState.ReviewingBootloader(review.versions) + } } + } catch (e: CancellationException) { + endMaintenanceSequence() + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.w(e) { "Reading the bootloader drive failed" } + reofferOrFail(pass, UiText.Resource(Res.string.firmware_update_failed)) } + } + } + + @Suppress("SuspendFunSwallowedCancellation") // cancellation hands the device back, then is rethrown + private fun launchPassWrite(pass: UsbFileSavePass, treeUri: CommonUri, hardware: DeviceHardware) { + maintenanceWriteJob = viewModelScope.launch { + try { + // Capture the ports present before the write so the erase image's port can be told from + // pre-existing + // ones. + val portsBefore = usbManager.serialPortKeys() + val result = usbPassWriter(portsBefore).write(pass, treeUri, hardware) { _state.value = it } + handlePassResult(pass, result) + } catch (e: CancellationException) { + // The write has stopped, so the device can be handed back. Doing this here rather than in + // cancelUpdate() is the point: the sequence keeps the radio until the write actually ends. + endMaintenanceSequence() + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.e(e) { "Writing ${pass.step} failed" } + reofferOrFail(pass, UiText.Resource(Res.string.firmware_update_failed)) + } + } } private suspend fun handlePassResult(pass: UsbFileSavePass, result: UsbPassResult) = when (result) { @@ -716,8 +792,8 @@ class FirmwareUpdateViewModel( reofferOrFail(pass, UiText.Resource(Res.string.firmware_maintenance_cdc_unblock_failed)) } - private suspend fun advancePastPass(pass: UsbFileSavePass) { - if (pass.step.isDestructive) destructiveWriteDone = true + private suspend fun advancePastPass(pass: UsbFileSavePass, written: Boolean = true) { + if (written && pass.step.isDestructive) destructiveWriteDone = true pendingUsbPasses = pendingUsbPasses.drop(1) val next = pendingUsbPasses.firstOrNull() @@ -811,45 +887,43 @@ class FirmwareUpdateViewModel( fun prepareLocalFirmwareFile(uri: CommonUri) { val currentState = _state.value as? FirmwareUpdateState.Ready ?: return clearPendingLocalFirmwareFile() - prepareJob = - viewModelScope.launch { - try { - val fileName = - safeCatching { fileHandler.getDisplayName(uri)?.takeIf { it.isNotBlank() } } - .getOrElse { e -> - Logger.w(e) { "Failed to resolve local firmware filename" } - null - } + prepareJob = viewModelScope.launch { + try { + val fileName = safeCatching { + fileHandler.getDisplayName(uri)?.takeIf { it.isNotBlank() } + } + .getOrElse { e -> + Logger.w(e) { "Failed to resolve local firmware filename" } + null + } - // State may have changed during the suspend call (e.g. cancelUpdate, checkForUpdates). - // Do not write errors or reopen the confirmation dialog for a stale selection. - when { - _state.value != currentState -> Unit + // State may have changed during the suspend call (e.g. cancelUpdate, checkForUpdates). + // Do not write errors or reopen the confirmation dialog for a stale selection. + when { + _state.value != currentState -> Unit - fileName == null -> - _state.value = - FirmwareUpdateState.Error( - UiText.Resource(Res.string.firmware_update_filename_unavailable), - ) + fileName == null -> + _state.value = + FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_filename_unavailable)) - else -> { - val resolution = resolveLocalFirmwareFile(uri, fileName, currentState) - if (_state.value != currentState) { - cleanupResolvedLocalFirmwareFile(resolution) - } else { - applyLocalFirmwareResolution(resolution, currentState) - } + else -> { + val resolution = resolveLocalFirmwareFile(uri, fileName, currentState) + if (_state.value != currentState) { + cleanupResolvedLocalFirmwareFile(resolution) + } else { + applyLocalFirmwareResolution(resolution, currentState) } } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.e(e) { "Error preparing local firmware file" } - if (_state.value == currentState) { - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(e) { "Error preparing local firmware file" } + if (_state.value == currentState) { + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) } } + } } fun confirmLocalFirmwareFile() { @@ -907,7 +981,10 @@ class FirmwareUpdateViewModel( LocalFirmwareResolution.Invalid(reason = fallbackReason, fileName = fileName) } else { val extractingState = - FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_extracting))) + FirmwareUpdateState.Processing( + ProgressState(UiText.Resource(Res.string.firmware_update_extracting)), + beforeConfirmation = true, + ) _state.value = extractingState try { val extractedArtifact = extractLocalFirmwareArchive(uri, fileName, state, payloadExtension) @@ -1036,49 +1113,48 @@ class FirmwareUpdateViewModel( originalDeviceAddress = radioPrefs.devAddr.value updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - val updateArtifact = - firmwareUpdateManager.startUpdate( - release = FirmwareRelease(id = LOCAL_RELEASE_ID, zipUrl = "", releaseNotes = ""), - hardware = currentState.deviceHardware, - address = currentState.address, - updateState = { _state.value = it }, - firmwareUri = uri, - ) - tempFirmwareFile = updateArtifact?.takeIf { it.isTemporary } ?: pendingArtifact - // If the handler created its own temp copy (e.g. ESP32 importFromUri), - // clean up the extracted bundle artifact to prevent a leak. - if (pendingArtifact != null && pendingArtifact != tempFirmwareFile) { - cleanupTemporaryFiles(fileHandler, pendingArtifact) - } - - when (val finalState = _state.value) { - is FirmwareUpdateState.Success -> - verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) - - // USB/UF2 path pauses here for the user to pick a save location; saveDfuFile() resumes it. - is FirmwareUpdateState.AwaitingFileSave -> Unit - - is FirmwareUpdateState.Error -> { - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } - - else -> { - Logger.w { "Firmware update returned without terminal state: ${_state.value}" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.e(e) { "Error starting update from file" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile ?: pendingArtifact) + updateJob = viewModelScope.launch { + try { + val updateArtifact = + firmwareUpdateManager.startUpdate( + release = FirmwareRelease(id = LOCAL_RELEASE_ID, zipUrl = "", releaseNotes = ""), + hardware = currentState.deviceHardware, + address = currentState.address, + updateState = { _state.value = it }, + firmwareUri = uri, + ) + tempFirmwareFile = updateArtifact?.takeIf { it.isTemporary } ?: pendingArtifact + // If the handler created its own temp copy (e.g. ESP32 importFromUri), + // clean up the extracted bundle artifact to prevent a leak. + if (pendingArtifact != null && pendingArtifact != tempFirmwareFile) { + cleanupTemporaryFiles(fileHandler, pendingArtifact) } + + when (val finalState = _state.value) { + is FirmwareUpdateState.Success -> + verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer) + + // USB/UF2 path pauses here for the user to pick a save location; saveDfuFile() resumes it. + is FirmwareUpdateState.AwaitingFileSave -> Unit + + is FirmwareUpdateState.Error -> { + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + } + + else -> { + Logger.w { "Firmware update returned without terminal state: ${_state.value}" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(e) { "Error starting update from file" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) + tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile ?: pendingArtifact) } + } } private fun localFirmwareValidationError( @@ -1244,7 +1320,7 @@ class FirmwareUpdateViewModel( } } - private suspend fun checkBatteryLevel(): Boolean { + private fun checkBatteryLevel(): Boolean { val node = nodeRepository.ourNodeInfo.value ?: return true val level = node.batteryLevel ?: 1 val isBatteryLow = level in 1..MIN_BATTERY_LEVEL @@ -1319,7 +1395,7 @@ private fun isValidBluetoothAddress(address: String?): Boolean = address != null && BLUETOOTH_ADDRESS_REGEX.matches(address) private fun isBluetoothInterfaceAddress(address: String): Boolean = - address.startsWith(InterfaceId.BLUETOOTH.id) || address.startsWith("!") + DeviceAddress.parse(address)?.interfaceId == InterfaceId.BLUETOOTH private fun FirmwareReleaseRepository.getReleaseFlow(type: FirmwareReleaseType): Flow = when (type) { FirmwareReleaseType.STABLE -> stableRelease diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/MaintenanceUf2.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/MaintenanceUf2.kt index baf8baebd5..4fed03042a 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/MaintenanceUf2.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/MaintenanceUf2.kt @@ -14,6 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +@file:Suppress("TooManyFunctions") + package org.meshtastic.feature.firmware import org.meshtastic.core.model.DeviceHardware @@ -185,6 +187,25 @@ internal fun parseUf2BoardId(infoUf2Text: String): String? = infoUf2Text ?.trim() ?.takeIf { it.isNotEmpty() } +/** + * Extracts the installed bootloader version from the contents of a UF2 bootloader's `INFO_UF2.TXT`. + * + * `ghostfat.c` writes the bootloader's git tag as the first token after `UF2 Bootloader` (e.g. `0.9.2-OTAFIX2.5`), + * followed by submodule versions. The 0.4.x line also carries a `Ver:` line, read only when the first line is absent. + * Returns `null` when neither line yields a token. + */ +internal fun parseUf2BootloaderVersion(infoUf2Text: String): String? = + listOf(UF2_BOOTLOADER_PREFIX, UF2_VER_PREFIX).firstNotNullOfOrNull { prefix -> + infoUf2Text + .lineSequence() + .map { it.trim() } + .firstOrNull { it.startsWith(prefix, ignoreCase = true) } + ?.drop(prefix.length) + ?.trim() + ?.substringBefore(' ') + ?.takeIf { it.isNotEmpty() } + } + /** * Extracts the installed SoftDevice from the contents of a UF2 bootloader's `INFO_UF2.TXT`. * @@ -288,6 +309,10 @@ internal fun resolveNrfEraseImage( /** The file every Adafruit-family UF2 bootloader exposes on its mass-storage volume. */ internal const val INFO_UF2_FILE_NAME = "INFO_UF2.TXT" +private const val UF2_BOOTLOADER_PREFIX = "UF2 Bootloader " + +private const val UF2_VER_PREFIX = "Ver:" + private const val UF2_BOARD_ID_PREFIX = "Board-ID:" private const val UF2_SOFTDEVICE_PREFIX = "SoftDevice:" diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbMaintenance.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbMaintenance.kt index b046382b7a..c84d8f9673 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbMaintenance.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbMaintenance.kt @@ -148,13 +148,28 @@ internal fun usbMaintenanceRefusalMessage(reason: UsbMaintenanceRefusal): UiText * @property eraseRefusal Non-null when erase is shown but cannot run; the reason is displayed and the action disabled. * @property showBootloaderUpgrade Whether a bootloader-upgrade action is offered. Absent (not refused) when no image is * mapped for the board — an unmapped board is a coverage gap, not a safety decision the user can act on. + * @property latestBootloader The OTAFIX release the upgrade installs, shown next to the action. The installed version + * is unknowable here: only the mounted drive reports it. */ data class UsbMaintenanceGate( val show: Boolean = false, val eraseRefusal: UsbMaintenanceRefusal? = null, val showBootloaderUpgrade: Boolean = false, + val latestBootloader: String? = null, ) +/** + * The bootloader a mounted drive reports next to the one the upgrade would install. + * + * @property installed From the drive's `INFO_UF2.TXT`, or `null` when it reports none. + * @property available [MaintenanceUf2Manifest.otafixReleaseTag]. + */ +data class BootloaderVersions(val installed: String?, val available: String) { + /** Exact match only: bench and vendor builds carry tags with no order to trust, so anything else just differs. */ + val isCurrent: Boolean + get() = installed != null && installed == available +} + /** * Decides which maintenance actions are available for [hardware] on [updateMethod]. * @@ -188,12 +203,14 @@ internal fun usbMaintenanceGate( else -> UsbMaintenanceRefusal.MaintenanceDataUnavailable } + // nRF-only: RP2040 boards run no Adafruit bootloader, so OTAFIX does not apply. This is a visibility hint only + // — which image gets written is decided later from the Board-ID the drive reports. + val showBootloaderUpgrade = hardware.isNrf52Arc && otafixSupportsTarget(manifest, hardware.effectiveTarget) return UsbMaintenanceGate( show = true, eraseRefusal = eraseRefusal, - // nRF-only: RP2040 boards run no Adafruit bootloader, so OTAFIX does not apply. This is a visibility hint only - // — which image gets written is decided later from the Board-ID the drive reports. - showBootloaderUpgrade = hardware.isNrf52Arc && otafixSupportsTarget(manifest, hardware.effectiveTarget), + showBootloaderUpgrade = showBootloaderUpgrade, + latestBootloader = manifest.otafixReleaseTag.takeIf { showBootloaderUpgrade && it.isNotBlank() }, ) } @@ -206,11 +223,14 @@ internal fun usbMaintenanceGate( * @property factoryEraseFamily The UF2 family ID the bootloader will consume as a factory-erase command (its * `Factory-Erase:` line), when it advertises one. `null` on every bootloader shipped before OTAFIX PR #41 — those * silently ignore the file, so `null` means "use the SoftDevice-specific sketch", never "refuse". + * @property bootloaderVersion The installed bootloader's version, from the `UF2 Bootloader` line. The running firmware + * cannot report it, so this drive is the only place the app learns it. */ internal data class MaintenanceVolume( val boardId: String, val softDevice: SoftDeviceVariant?, val factoryEraseFamily: Long? = null, + val bootloaderVersion: String? = null, ) /** Outcome of vetting a user-picked volume before anything is written to it. */ @@ -243,10 +263,32 @@ internal suspend fun inspectMaintenanceVolume(treeUri: CommonUri, fileHandler: F boardId = boardId, softDevice = parseUf2SoftDevice(info), factoryEraseFamily = parseUf2FactoryEraseFamily(info), + bootloaderVersion = parseUf2BootloaderVersion(info), ), ) } +/** What the drive says before a bootloader upgrade is written, or why the upgrade cannot run on it. */ +internal sealed interface BootloaderReview { + data class Ready(val versions: BootloaderVersions) : BootloaderReview + + data class Refused(val reason: UsbMaintenanceRefusal) : BootloaderReview +} + +/** + * Compares the bootloader [volume] reports against [manifest]'s release, once the drive has been read and before + * anything is downloaded or written. + * + * Refuses an unrecognized Board-ID here, with the same outcome [chooseMaintenanceImage] would reach at write time, so + * the user is never shown an upgrade that cannot run. + */ +internal fun reviewBootloader(manifest: MaintenanceUf2Manifest, volume: MaintenanceVolume): BootloaderReview = + if (otafixUf2ForBoardId(manifest, volume.boardId) == null) { + BootloaderReview.Refused(UsbMaintenanceRefusal.UnknownBoardId) + } else { + BootloaderReview.Ready(BootloaderVersions(volume.bootloaderVersion, manifest.otafixReleaseTag)) + } + /** Which image to write, or why not. */ internal sealed interface MaintenanceImageChoice { data class Resolved(val asset: MaintenanceUf2) : MaintenanceImageChoice diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt index 4b8235f6eb..80c7c23ef0 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt @@ -18,8 +18,8 @@ package org.meshtastic.feature.firmware import org.koin.core.annotation.Single import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioController diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateSupport.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateSupport.kt index 34357d9b1d..8980e4da8c 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateSupport.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateSupport.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.MaintenanceUf2Repository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioController @@ -35,9 +35,9 @@ import org.meshtastic.core.resources.firmware_update_rebooting import org.meshtastic.core.resources.firmware_update_retrieval_failed import org.meshtastic.core.resources.firmware_update_usb_failed import org.meshtastic.core.resources.getStringSuspend +import org.meshtastic.feature.firmware.ota.formatTransferPercent private const val USB_REBOOT_DELAY = 5000L -private const val PERCENT_MAX = 100 /** * One leg of a multi-pass USB/UF2 sequence. @@ -94,13 +94,12 @@ internal suspend fun performUsbMaintenance( val firmware = try { retrieveUsbFirmware(release, hardware) { progress -> - val percent = (progress * PERCENT_MAX).toInt() updateState( FirmwareUpdateState.Downloading( ProgressState( message = UiText.DynamicString(downloadingMsg), progress = progress, - details = "$percent%", + details = formatTransferPercent(progress), ), ), ) @@ -128,7 +127,10 @@ internal suspend fun performUsbMaintenance( } updateState(FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_rebooting)))) - radioController.rebootToDfu(nodeRepository.myNodeInfo.value?.myNodeNum ?: 0) + radioController.rebootToDfu( + nodeRepository.myNodeInfo.value?.myNodeNum ?: 0, + radioController.generatePacketId(), + ) delay(USB_REBOOT_DELAY) val passes = @@ -221,10 +223,11 @@ internal class UsbPassWriter( return UsbPassResult.CopyFailed } - val copied = - safeCatching { fileHandler.copyToUri(artifact, destination) } - .onFailure { Logger.e(it) { "Copying $fileName to the UF2 volume failed" } } - .getOrNull() + val copied = safeCatching { + fileHandler.copyToUri(artifact, destination) + } + .onFailure { Logger.e(it) { "Copying $fileName to the UF2 volume failed" } } + .getOrNull() if (copied == null) return UsbPassResult.CopyFailed updateState(FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_flashing)))) @@ -255,6 +258,16 @@ internal class UsbPassWriter( return UsbPassResult.Written } + /** + * Reads [treeUri] for a bootloader upgrade and compares what it reports against the release. Writes nothing; + * [write] vets the volume again once the user confirms. + */ + suspend fun review(treeUri: CommonUri): BootloaderReview = + when (val inspection = inspectMaintenanceVolume(treeUri, fileHandler)) { + is VolumeInspection.Rejected -> BootloaderReview.Refused(inspection.reason) + is VolumeInspection.Accepted -> reviewBootloader(maintenanceUf2Repository.getSnapshot(), inspection.volume) + } + /** Either the image to write, or the result to return instead. */ private sealed interface ImageResolution { /** @property requiresCdcUnblock Carried from [MaintenanceUf2.requiresCdcUnblock]; always false for firmware. */ @@ -292,6 +305,7 @@ internal class UsbPassWriter( ProgressState( message = UiText.DynamicString(downloadingMsg), progress = progress, + details = formatTransferPercent(progress), ), ), ) @@ -352,7 +366,7 @@ internal suspend fun performUsbUpdate( FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_rebooting))), ) val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0 - radioController.rebootToDfu(myNodeNum) + radioController.rebootToDfu(myNodeNum, radioController.generatePacketId()) delay(USB_REBOOT_DELAY) val sourceArtifact = @@ -362,13 +376,12 @@ internal suspend fun performUsbUpdate( } else { val firmwareFile = retrieveUsbFirmware(release, hardware) { progress -> - val percent = (progress * PERCENT_MAX).toInt() updateState( FirmwareUpdateState.Downloading( ProgressState( message = UiText.DynamicString(downloadingMsg), progress = progress, - details = "$percent%", + details = formatTransferPercent(progress), ), ), ) @@ -386,7 +399,7 @@ internal suspend fun performUsbUpdate( val processingState = ProgressState(UiText.Resource(Res.string.firmware_update_rebooting)) updateState(FirmwareUpdateState.Processing(processingState)) val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0 - radioController.rebootToDfu(myNodeNum) + radioController.rebootToDfu(myNodeNum, radioController.generatePacketId()) delay(USB_REBOOT_DELAY) val fileName = firmwareFile.fileName ?: "firmware.uf2" diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt index 57bd6fff2a..953c4cf3c7 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt @@ -17,6 +17,7 @@ package org.meshtastic.feature.firmware.ota import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -393,6 +394,7 @@ class BleOtaTransport( packetsSent++ } } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + if (e is CancellationException) currentCoroutineContext().ensureActive() throw OtaProtocolException.TransferFailed("Failed to write data at offset $offset", e) } return packetsSent diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt index d22e96fb65..fec15c68b8 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt @@ -28,9 +28,9 @@ import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.ioDispatcher -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.util.isOtaStatusNotification import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.NodeRepository @@ -59,7 +59,6 @@ import org.meshtastic.feature.firmware.ProgressState import org.meshtastic.feature.firmware.stripFormatArgs private const val RETRY_DELAY = 2000L -private const val PERCENT_MAX = 100 private const val REBOOT_MODE_BLE = 1 private const val REBOOT_MODE_WIFI = 2 @@ -361,13 +360,12 @@ class Esp32OtaUpdateHandler( val firmwareFile = firmwareRetriever.retrieveEsp32Firmware(release, hardware) { progress -> - val percent = (progress * PERCENT_MAX).toInt() updateState( FirmwareUpdateState.Downloading( ProgressState( message = UiText.DynamicString(downloadingMsg), progress = progress, - details = "$percent%", + details = formatTransferPercent(progress), ), ), ) @@ -467,6 +465,7 @@ class Esp32OtaUpdateHandler( return connectedTransport ?: throw postConfirmConnectionFailed(rebootMode, attempts, null) } + @Suppress("SuspendFunSwallowedCancellation") // cancellation closes the attempt's transport, then is rethrown private suspend fun connectTransportAttempt(transport: UnifiedOtaProtocol, rebootMode: Int): Result { val connectResult = try { @@ -490,6 +489,7 @@ class Esp32OtaUpdateHandler( return Result.failure(error) } + @Suppress("SuspendFunSwallowedCancellation") // the close runs under NonCancellable, so no cancellation reaches it private suspend fun closeFailedTransport(transport: UnifiedOtaProtocol) { withContext(NonCancellable) { try { diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpers.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpers.kt index 027f0c683b..0c65aec853 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpers.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpers.kt @@ -18,24 +18,36 @@ package org.meshtastic.feature.firmware.ota import kotlinx.coroutines.delay import org.meshtastic.core.ble.BleScanStartException -import org.meshtastic.core.common.util.NumberFormatter +import org.meshtastic.core.common.util.formatByteSize +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_transfer_percent +import org.meshtastic.core.resources.firmware_update_transfer_progress private const val PERCENT_MAX = 100 -private const val KIB_DIVISOR = 1024f + +/** Formats download or transfer progress as a translated bare percentage, e.g. `"42%"` in English. */ +internal fun formatTransferPercent(progress: Float): UiText = + UiText.Resource(Res.string.firmware_update_transfer_percent, (progress * PERCENT_MAX).toInt()) /** - * Formats firmware-transfer progress as a human-readable detail string, e.g. `"42% (12.3 KiB/s, ETA: 5s)"`. + * Formats firmware-transfer progress as translated detail text, e.g. `"42% (12.60 kB/s, ETA: 5s)"` in English, with the + * rate in decimal units. * * When [bytesPerSecond] is non-positive (no throughput sample yet) only the percentage is returned — no empty * parentheses. Shared by the ESP32 OTA and Nordic DFU update handlers, which differ only in how they obtain the inputs. */ -internal fun formatTransferProgress(progress: Float, totalBytes: Int, bytesPerSecond: Long): String { +internal fun formatTransferProgress(progress: Float, totalBytes: Int, bytesPerSecond: Long): UiText { + if (bytesPerSecond <= 0L) return formatTransferPercent(progress) val percent = (progress * PERCENT_MAX).toInt() - if (bytesPerSecond <= 0L) return "$percent%" - val kibPerSecond = bytesPerSecond.toFloat() / KIB_DIVISOR val bytesSent = (progress * totalBytes).toLong() val etaSeconds = ((totalBytes - bytesSent).toFloat() / bytesPerSecond).toInt() - return "$percent% (${NumberFormatter.format(kibPerSecond, 1)} KiB/s, ETA: ${etaSeconds}s)" + return UiText.Resource( + Res.string.firmware_update_transfer_progress, + percent, + formatByteSize(bytesPerSecond), + etaSeconds, + ) } /** diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/ThroughputTracker.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/ThroughputTracker.kt index 82b5adcc4d..eef2d08867 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/ThroughputTracker.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/ThroughputTracker.kt @@ -16,10 +16,9 @@ */ package org.meshtastic.feature.firmware.ota +import org.meshtastic.core.model.util.TimeConstants import kotlin.time.TimeSource -private const val MILLIS_PER_SECOND = 1000L - /** * Sliding window throughput tracker to calculate current transfer speed in bytes per second. Adapted from kmp-ble's * DfuProgress throughput tracking. @@ -52,6 +51,6 @@ class ThroughputTracker(private val windowSize: Int = 10, private val timeSource if (durationMs <= 0) return 0 val deltaBytes = byteCounts[newestIdx] - byteCounts[oldestIdx] - return (deltaBytes * MILLIS_PER_SECOND) / durationMs + return (deltaBytes * TimeConstants.MS_PER_SEC) / durationMs } } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocol.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocol.kt index ae1d370464..268f10f4c4 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocol.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocol.kt @@ -88,7 +88,7 @@ internal object LegacyDfuStatus { DATA_SIZE_EXCEEDS_LIMIT -> "DATA_SIZE_EXCEEDS_LIMIT" CRC_ERROR -> "CRC_ERROR" OPERATION_FAILED -> "OPERATION_FAILED" - else -> "UNKNOWN(0x${status.toUByte().toString(16).padStart(2, '0')})" + else -> "UNKNOWN(0x${status.toHexString()})" } } @@ -216,7 +216,7 @@ sealed class LegacyDfuException(message: String, cause: Throwable? = null) : Dfu /** Device returned a non-success status for a given opcode. */ class ProtocolError(val requestOpcode: Byte, val status: Byte) : LegacyDfuException( - "Legacy DFU protocol error: opcode=0x${requestOpcode.toUByte().toString(16).padStart(2, '0')} " + + "Legacy DFU protocol error: opcode=0x${requestOpcode.toHexString()} " + "status=${LegacyDfuStatus.describe(status)}", ) diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransport.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransport.kt index 3bca01469c..5c8f5e6d35 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransport.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransport.kt @@ -166,12 +166,13 @@ internal constructor( // Best-effort DFU Version read — gate out unsupported old bootloaders (SDK ≤ 6). val versionChar = service.characteristic(LEGACY_DFU_VERSION_UUID) - val version = - safeCatching { service.read(versionChar) } - .map { bytes -> - if (bytes.size >= 2) (bytes[0].toInt() and 0xFF) or ((bytes[1].toInt() and 0xFF) shl 8) else -1 - } - .getOrElse { -1 } + val version = safeCatching { + service.read(versionChar) + } + .map { bytes -> + if (bytes.size >= 2) (bytes[0].toInt() and 0xFF) or ((bytes[1].toInt() and 0xFF) shl 8) else -1 + } + .getOrElse { -1 } dfuVersion = version Logger.i { "Legacy DFU: DFU Version characteristic = $version (-1 ⇒ absent / unreadable)" } if (version in 1..MIN_SUPPORTED_DFU_VERSION - 1) { @@ -335,7 +336,8 @@ internal constructor( * the bootloader's bytes-received count. The [streamOffset], [streamLastPrnOffset], and [streamLastPrnLatencyMs] * snapshots give the watcher's onDrop callback visible diagnostic values. */ - @Suppress("CyclomaticComplexMethod", "NestedBlockDepth", "LongMethod") + // Cancellations are logged with the stream offset for diagnosis, then rethrown. + @Suppress("CyclomaticComplexMethod", "NestedBlockDepth", "LongMethod", "SuspendFunSwallowedCancellation") private suspend fun streamFirmware(firmware: ByteArray, onProgress: suspend (Float) -> Unit) { // Packet size = negotiated ATT MTU − 3, word-aligned and capped at 244 (see computeStreamPacketSize). Falls // back to 20 bytes when the bootloader did not negotiate a larger MTU, which is the self-gating safety against @@ -468,6 +470,7 @@ internal constructor( * * Parent cancellation is preserved: a [CancellationException] that escapes `withTimeoutOrNull` is propagated. */ + @Suppress("SuspendFunSwallowedCancellation") // the cancellation is logged, then rethrown override suspend fun abort() { val write = try { @@ -606,8 +609,8 @@ internal constructor( if (response.requestOpcode != expectedOpcode) { throw DfuException.TransferFailed( "Legacy DFU response opcode mismatch: expected " + - "0x${expectedOpcode.toUByte().toString(16).padStart(2, '0')}, " + - "got 0x${response.requestOpcode.toUByte().toString(16).padStart(2, '0')}", + "0x${expectedOpcode.toHexString()}, " + + "got 0x${response.requestOpcode.toHexString()}", ) } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt index cc8ce67b23..8eba78fcd5 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuHandler.kt @@ -28,9 +28,9 @@ import org.meshtastic.core.ble.BleScanStartException import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.ioDispatcher -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.RadioController import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText @@ -55,6 +55,7 @@ import org.meshtastic.feature.firmware.FirmwareUpdateState import org.meshtastic.feature.firmware.ProgressState import org.meshtastic.feature.firmware.ota.ThroughputTracker import org.meshtastic.feature.firmware.ota.calculateMacPlusOne +import org.meshtastic.feature.firmware.ota.formatTransferPercent import org.meshtastic.feature.firmware.ota.formatTransferProgress import org.meshtastic.feature.firmware.ota.retryWithDelay import org.meshtastic.feature.firmware.ota.scanForBleDevice @@ -62,7 +63,6 @@ import org.meshtastic.feature.firmware.stripFormatArgs import kotlin.time.Duration.Companion.seconds import kotlin.uuid.Uuid -private const val PERCENT_MAX = 100 private const val GATT_RELEASE_DELAY_MS = 1_500L private const val DFU_REBOOT_WAIT_MS = 3_000L private const val RETRY_DELAY_MS = 2_000L @@ -180,7 +180,7 @@ internal class DfuFallbackCoordinator(private val detection: BootloaderDetection } } } - throw IllegalStateException("DFU fallback exhausted with non-empty protocol list (detection=$detection)") + error("DFU fallback exhausted with non-empty protocol list (detection=$detection)") } /** @@ -834,10 +834,9 @@ class SecureDfuHandler( val path = firmwareRetriever.retrieveOtaFirmware(release, hardware) { progress -> - val pct = (progress * PERCENT_MAX).toInt() updateState( FirmwareUpdateState.Downloading( - ProgressState(UiText.DynamicString(downloadingMsg), progress, "$pct%"), + ProgressState(UiText.DynamicString(downloadingMsg), progress, formatTransferPercent(progress)), ), ) } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuProtocol.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuProtocol.kt index f47627df51..e98df63cf1 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuProtocol.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuProtocol.kt @@ -131,7 +131,7 @@ internal object DfuExtendedError { WRONG_SIGNATURE_TYPE -> "Wrong signature type" VERIFICATION_FAILED -> "Verification failed" INSUFFICIENT_SPACE -> "Insufficient space" - else -> "Unknown extended error 0x${code.toUByte().toString(16).padStart(2, '0')}" + else -> "Unknown extended error 0x${code.toHexString()}" } } @@ -296,8 +296,8 @@ sealed class DfuException(message: String, cause: Throwable? = null) : Exception class ProtocolError(val opcode: Byte, val resultCode: Byte, val extendedError: Byte? = null) : DfuException( buildString { - append("DFU protocol error: opcode=0x${opcode.toUByte().toString(16).padStart(2, '0')} ") - append("result=0x${resultCode.toUByte().toString(16).padStart(2, '0')}") + append("DFU protocol error: opcode=0x${opcode.toHexString()} ") + append("result=0x${resultCode.toHexString()}") if (extendedError != null) { append(" ext=${DfuExtendedError.describe(extendedError)}") } @@ -306,10 +306,7 @@ sealed class DfuException(message: String, cause: Throwable? = null) : Exception /** CRC-32 of the transferred data does not match the device's computed checksum. */ class ChecksumMismatch(expected: Int, actual: Int) : - DfuException( - "CRC-32 mismatch: expected 0x${expected.toUInt().toString(16).padStart(8, '0')} " + - "got 0x${actual.toUInt().toString(16).padStart(8, '0')}", - ) + DfuException("CRC-32 mismatch: expected 0x${expected.toHexString()} got 0x${actual.toHexString()}") /** A DFU operation did not complete within the expected time window. */ class Timeout(message: String) : DfuException(message) diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransport.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransport.kt index a4e5eebfaa..a3c0ac6469 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransport.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransport.kt @@ -34,7 +34,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -144,7 +146,8 @@ class SecureDfuTransport( } } catch (_: TimeoutCancellationException) { Logger.d { "DFU: No buttonless indication received (device may have already disconnected)" } - } catch (_: Exception) { + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + if (e is CancellationException) currentCoroutineContext().ensureActive() Logger.d { "DFU: Buttonless indication wait interrupted (device disconnecting)" } } }, diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt index 5e5d6d144a..d427ddce47 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.kt @@ -14,18 +14,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.feature.firmware.ota.FirmwareHashUtil import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -247,6 +247,27 @@ abstract class CommonFirmwareRetrieverTest { assertNull(result, "A failed zip download must resolve to null, not propagate") } + @Test + fun `cancelling the release zip download propagates instead of resolving to null`() = runTest { + val handler = FakeFirmwareFileHandler() + val retriever = FirmwareRetriever(handler) + handler.zipDownloadException = CancellationException("update cancelled") + + assertFailsWith { retriever.retrieveEsp32Firmware(TEST_RELEASE, TEST_HARDWARE) {} } + } + + @Test + fun `cancelling a direct download does not fall back to the release zip`() = runTest { + val handler = FakeFirmwareFileHandler() + val retriever = FirmwareRetriever(handler) + // The first direct download tried (the -update.bin fallback) is the one cancelled. + handler.existingUrls.add("$RELEASE_BASE_URL/2.7.17/firmware-heltec-v3-2.7.17-update.bin") + handler.directDownloadException = CancellationException("update cancelled") + + assertFailsWith { retriever.retrieveEsp32Firmware(TEST_RELEASE, TEST_HARDWARE) {} } + assertTrue(handler.downloadedUrls.none { it.endsWith(".zip") }, "zip fallback ran: ${handler.downloadedUrls}") + } + @Test fun `retrieveEsp32Firmware returns null when all strategies fail`() = runTest { val handler = FakeFirmwareFileHandler() @@ -695,6 +716,9 @@ abstract class CommonFirmwareRetrieverTest { /** When set, [downloadFile] throws this for the "firmware_release.zip" download instead of returning. */ var zipDownloadException: Exception? = null + /** When set, [downloadFile] throws this for every direct (non-zip) download instead of returning. */ + var directDownloadException: Exception? = null + /** Result returned by [extractFirmwareFromZip]. */ var zipExtractionResult: FirmwareArtifact? = null @@ -733,6 +757,7 @@ abstract class CommonFirmwareRetrieverTest { } // Direct download: only succeed if the URL was registered as existing + directDownloadException?.let { throw it } return if (url in existingUrls) { FirmwareArtifact( uri = CommonUri.parse("file:///tmp/$fileName"), diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt index e98a38136a..75a6646e84 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt @@ -35,115 +35,116 @@ import kotlin.test.assertTrue */ abstract class CommonMaintenanceVolumeTest { + private val manifestJson = Json { ignoreUnknownKeys = true } + /** The real maintenance-UF2 manifest, embedded verbatim — see UsbMaintenanceGateTest for why. */ private val testManifest = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "manifestVersion": 1, - "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", - "otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5", - "erase": { - "nrf52": { - "6.1.1": { - "fileName": "nrf_erase2.uf2", - "sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe", - "expectedFirstTargetAddress": 155648 - }, - "7.3.0": { - "fileName": "nrf_erase_sd7_3.uf2", - "sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f", - "expectedFirstTargetAddress": 159744 - } - }, - "nrf52Bootloader": { - "fileName": "meshtastic_factory_erase.uf2", - "sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d", - "expectedFamilyId": 1296388936 - }, - "rp2040": { - "fileName": "pico_erase.uf2", - "sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853" - } + manifestJson.decodeFromString( + """ + { + "manifestVersion": 1, + "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", + "otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5", + "erase": { + "nrf52": { + "6.1.1": { + "fileName": "nrf_erase2.uf2", + "sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe", + "expectedFirstTargetAddress": 155648 }, - "otafixByBoardId": { - "HT-n5262": { - "otafixBoardSlug": "heltec_t114", - "sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19" - }, - "MinewSemi-MX25LE01": { - "otafixBoardSlug": "minewsemi_mx25le01", - "sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b" - }, - "TRACKER L1": { - "otafixBoardSlug": "wio_tracker_l1", - "sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e" - }, - "WisBlock-RAK4631-Board": { - "otafixBoardSlug": "wiscore_rak4631_board", - "sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305" - }, - "WisMesh-Tag": { - "otafixBoardSlug": "wismesh_tag", - "sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758" - }, - "nRF52840-SeeedSenseCAPSolarP1-v1": { - "otafixBoardSlug": "sensecap_solar_p1", - "sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5" - }, - "nRF52840-SeeedXiao-v1": { - "otafixBoardSlug": "xiao_nrf52840_ble", - "sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f" - }, - "nRF52840-SeeedXiaoSense-v1": { - "otafixBoardSlug": "xiao_nrf52840_ble_sense", - "sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da" - }, - "nRF52840-T1000-E-v1": { - "otafixBoardSlug": "t1000_e", - "sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4" - }, - "nRF52840-TEcho-v1": { - "otafixBoardSlug": "lilygo_techo", - "sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0" - }, - "nRF52840-ThinkNode-M3-v1": { - "otafixBoardSlug": "thinknode_m3", - "sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708" - }, - "nRF52840-ThinkNodeM1-v1": { - "otafixBoardSlug": "thinknode_m1", - "sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795" - }, - "nRF52840-ThinkNodeM6-v1": { - "otafixBoardSlug": "thinknode_m6", - "sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a" - }, - "nRF52840-promicro": { - "otafixBoardSlug": "promicro_nrf52840", - "sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794" - } - }, - "otafixSupportedTargets": [ - "rak4631", - "rak_wismeshtag", - "t-echo", - "heltec-mesh-node-t114", - "nrf52_promicro_diy_tcxo", - "thinknode_m1", - "thinknode_m3", - "thinknode_m6", - "tracker-t1000-e", - "seeed_wio_tracker_L1", - "seeed_wio_tracker_L1_eink", - "seeed_solar_node", - "seeed_xiao_nrf52840_kit" - ] + "7.3.0": { + "fileName": "nrf_erase_sd7_3.uf2", + "sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f", + "expectedFirstTargetAddress": 159744 + } + }, + "nrf52Bootloader": { + "fileName": "meshtastic_factory_erase.uf2", + "sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d", + "expectedFamilyId": 1296388936 + }, + "rp2040": { + "fileName": "pico_erase.uf2", + "sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853" } - """ - .trimIndent(), - ) + }, + "otafixByBoardId": { + "HT-n5262": { + "otafixBoardSlug": "heltec_t114", + "sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19" + }, + "MinewSemi-MX25LE01": { + "otafixBoardSlug": "minewsemi_mx25le01", + "sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b" + }, + "TRACKER L1": { + "otafixBoardSlug": "wio_tracker_l1", + "sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e" + }, + "WisBlock-RAK4631-Board": { + "otafixBoardSlug": "wiscore_rak4631_board", + "sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305" + }, + "WisMesh-Tag": { + "otafixBoardSlug": "wismesh_tag", + "sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758" + }, + "nRF52840-SeeedSenseCAPSolarP1-v1": { + "otafixBoardSlug": "sensecap_solar_p1", + "sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5" + }, + "nRF52840-SeeedXiao-v1": { + "otafixBoardSlug": "xiao_nrf52840_ble", + "sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f" + }, + "nRF52840-SeeedXiaoSense-v1": { + "otafixBoardSlug": "xiao_nrf52840_ble_sense", + "sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da" + }, + "nRF52840-T1000-E-v1": { + "otafixBoardSlug": "t1000_e", + "sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4" + }, + "nRF52840-TEcho-v1": { + "otafixBoardSlug": "lilygo_techo", + "sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0" + }, + "nRF52840-ThinkNode-M3-v1": { + "otafixBoardSlug": "thinknode_m3", + "sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708" + }, + "nRF52840-ThinkNodeM1-v1": { + "otafixBoardSlug": "thinknode_m1", + "sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795" + }, + "nRF52840-ThinkNodeM6-v1": { + "otafixBoardSlug": "thinknode_m6", + "sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a" + }, + "nRF52840-promicro": { + "otafixBoardSlug": "promicro_nrf52840", + "sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794" + } + }, + "otafixSupportedTargets": [ + "rak4631", + "rak_wismeshtag", + "t-echo", + "heltec-mesh-node-t114", + "nrf52_promicro_diy_tcxo", + "thinknode_m1", + "thinknode_m3", + "thinknode_m6", + "tracker-t1000-e", + "seeed_wio_tracker_L1", + "seeed_wio_tracker_L1_eink", + "seeed_solar_node", + "seeed_xiao_nrf52840_kit" + ] + } + """ + .trimIndent(), + ) private val treeUri = CommonUri.parse("content://com.android.externalstorage.documents/tree/1234-5678%3A") @@ -191,6 +192,7 @@ abstract class CommonMaintenanceVolumeTest { val accepted = assertIs(result) assertEquals("WisBlock-RAK4631-Board", accepted.volume.boardId) assertEquals(SoftDeviceVariant.S140_6_1_1, accepted.volume.softDevice) + assertEquals("0.4.3", accepted.volume.bootloaderVersion) } @Test diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonPerformUsbUpdateTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonPerformUsbUpdateTest.kt index cf4e3e155c..50e9faed1b 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonPerformUsbUpdateTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonPerformUsbUpdateTest.kt @@ -14,18 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_transfer_percent import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.TestDataFactory import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -209,10 +211,37 @@ abstract class CommonPerformUsbUpdateTest { val downloadingStates = states.filterIsInstance() assertTrue(downloadingStates.size >= 2, "Expected multiple Downloading states for progress updates") - assertTrue(downloadingStates.any { it.progressState.details == "25%" }, "Expected 25% progress detail") - assertTrue(downloadingStates.any { it.progressState.details == "75%" }, "Expected 75% progress detail") + assertEquals( + listOf(percentDetail(25), percentDetail(75)), + downloadingStates.mapNotNull { it.progressState.details }, + ) } + @Test + fun `maintenance download reports progress as a translated percentage`() = runTest { + val states = mutableListOf() + + performUsbMaintenance( + request = UsbMaintenanceRequest.FactoryErase, + release = testRelease, + hardware = testHardware, + radioController = FakeRadioController(), + nodeRepository = FakeNodeRepository(), + updateState = { states.add(it) }, + retrieveUsbFirmware = { _, _, onProgress -> + onProgress(0.4f) + null + }, + ) + + assertEquals( + listOf(percentDetail(40)), + states.filterIsInstance().mapNotNull { it.progressState.details }, + ) + } + + private fun percentDetail(percent: Int) = UiText.Resource(Res.string.firmware_update_transfer_percent, percent) + @Test fun `download path returns artifact for caller cleanup`() = runTest { val radioController = FakeRadioController() diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt index 9e8643f8b5..3640cc26f9 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt @@ -23,6 +23,9 @@ import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MaintenanceUf2Manifest import org.meshtastic.core.model.SoftDeviceVariant import org.meshtastic.core.repository.MaintenanceUf2Repository +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_transfer_percent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -38,33 +41,34 @@ import kotlin.test.assertTrue */ abstract class CommonUsbPassWriterTest { + private val manifestJson = Json { ignoreUnknownKeys = true } + private val manifest = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "manifestVersion": 1, - "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", - "otafixBase": "https://example.invalid/otafix", - "erase": { - "nrf52": { - "6.1.1": { "fileName": "nrf_erase2.uf2", "sha256": "00", "expectedFirstTargetAddress": 155648 } - }, - "nrf52Bootloader": { - "fileName": "meshtastic_factory_erase.uf2", - "sha256": "00", - "expectedFamilyId": 1296388936 - }, - "rp2040": { "fileName": "pico_erase.uf2", "sha256": "00" } - }, - "otafixByBoardId": { - "WisBlock-RAK4631-Board": { "otafixBoardSlug": "wiscore_rak4631_board", "sha256": "00" } - }, - "otafixSupportedTargets": ["rak4631"] - } - """ - .trimIndent(), - ) + manifestJson.decodeFromString( + """ + { + "manifestVersion": 1, + "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", + "otafixBase": "https://example.invalid/otafix", + "erase": { + "nrf52": { + "6.1.1": { "fileName": "nrf_erase2.uf2", "sha256": "00", "expectedFirstTargetAddress": 155648 } + }, + "nrf52Bootloader": { + "fileName": "meshtastic_factory_erase.uf2", + "sha256": "00", + "expectedFamilyId": 1296388936 + }, + "rp2040": { "fileName": "pico_erase.uf2", "sha256": "00" } + }, + "otafixByBoardId": { + "WisBlock-RAK4631-Board": { "otafixBoardSlug": "wiscore_rak4631_board", "sha256": "00" } + }, + "otafixSupportedTargets": ["rak4631"] + } + """ + .trimIndent(), + ) private val treeUri = CommonUri.parse("content://com.android.externalstorage.documents/tree/1234-5678%3A") @@ -108,7 +112,8 @@ abstract class CommonUsbPassWriterTest { UsbPassWriter( fileHandler = WritableVolume(info), maintenanceUf2Repository = FixedManifest(manifest), - retrieveMaintenanceUf2 = { asset, _ -> + retrieveMaintenanceUf2 = { asset, onProgress -> + onProgress(0.5f) written += asset.fileName FirmwareArtifact(uri = CommonUri.parse("file:///tmp/${asset.fileName}"), fileName = asset.fileName) }, @@ -135,6 +140,19 @@ abstract class CommonUsbPassWriterTest { assertEquals(1, h.unblockCalls.size, "The sketch blocks on while(!Serial) until DTR is asserted") } + @Test + fun `the maintenance image download shows its percent`() = runTest { + val h = harness(sketchInfo) + val states = mutableListOf() + + h.writer.write(erasePass, treeUri, rak) { states += it } + + assertEquals( + listOf(UiText.Resource(Res.string.firmware_update_transfer_percent, 50)), + states.filterIsInstance().map { it.progressState.details }, + ) + } + @Test fun `the bootloader erase image never has its cdc port opened`() = runTest { // After the bootloader consumes the block the only CDC port present is the bootloader's own; opening it would @@ -158,6 +176,23 @@ abstract class CommonUsbPassWriterTest { assertTrue(bootloader.unblockCalls.isEmpty()) } + @Test + fun `reviewing a bootloader upgrade reads the drive and writes nothing`() = runTest { + val h = harness(sketchInfo) + + val review = h.writer.review(treeUri) + + assertEquals(BootloaderReview.Ready(BootloaderVersions("0.4.3", "0.9.2-OTAFIX2.3-BP1.5")), review) + assertTrue(h.written.isEmpty(), "no image is fetched until the user confirms") + } + + @Test + fun `reviewing refuses a drive that is not a bootloader volume`() = runTest { + val h = harness("Model: Something\r\n") + + assertEquals(BootloaderReview.Refused(UsbMaintenanceRefusal.NotABootloaderVolume), h.writer.review(treeUri)) + } + @Test fun `a bootloader self-update never has its cdc port opened`() = runTest { val h = harness(sketchInfo) diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt index f9c010fce4..5e8a9fd7a8 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt @@ -34,17 +34,19 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.meshtastic.core.common.state.HiddenFeaturesUnlock import org.meshtastic.core.common.state.RadioOperationLock -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.datastore.FirmwareRecoveryDataSource import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.MaintenanceUf2Manifest import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FirmwareReleaseRepository +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.MaintenanceUf2Repository import org.meshtastic.core.repository.NodeRestartTracker import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioPrefs +import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.TestDataFactory @@ -130,6 +132,8 @@ class FirmwareUpdateIntegrationTest { HiddenFeaturesUnlock(), analytics, NodeRestartTracker(TestApplicationCoroutineScope(testDispatcher)), + FakeBluetoothRepository(), + FirmwareUpdateStatusRepository(), ) @Test diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateStateTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateStateTest.kt index e278403b15..50402fd488 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateStateTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateStateTest.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.feature.firmware +import org.meshtastic.core.repository.FirmwareUpdateProgress import org.meshtastic.core.resources.UiText import kotlin.test.Test import kotlin.test.assertEquals @@ -33,10 +34,10 @@ class FirmwareUpdateStateTest { @Test fun `ProgressState can be instantiated with values`() { - val state = ProgressState(UiText.DynamicString("Downloading"), 0.5f, "1MB/s") + val state = ProgressState(UiText.DynamicString("Downloading"), 0.5f, UiText.DynamicString("1MB/s")) assertTrue(state.message is UiText.DynamicString) assertEquals(0.5f, state.progress) - assertEquals("1MB/s", state.details) + assertEquals(UiText.DynamicString("1MB/s"), state.details) } @Test @@ -58,4 +59,29 @@ class FirmwareUpdateStateTest { fun `stripFormatArgs handles empty string`() { assertEquals("", "".stripFormatArgs()) } + + @Test + fun `writing and downloading report a percent the notification can show`() { + val message = UiText.DynamicString("Writing") + + assertEquals( + FirmwareUpdateProgress(message, percent = 42), + FirmwareUpdateState.Updating(ProgressState(message, 0.42f)).toUpdateProgress(), + ) + assertEquals(100, FirmwareUpdateState.Downloading(ProgressState(message, 1.3f)).toUpdateProgress()?.percent) + } + + @Test + fun `waits report no percent and states the user acts on report nothing`() { + val message = UiText.DynamicString("Waiting for reboot") + + assertEquals(null, FirmwareUpdateState.Processing(ProgressState(message, 0.9f)).toUpdateProgress()?.percent) + assertEquals(null, FirmwareUpdateState.Verifying.toUpdateProgress()?.percent) + assertEquals( + null, + FirmwareUpdateState.Processing(ProgressState(message), beforeConfirmation = true).toUpdateProgress(), + ) + assertEquals(null, FirmwareUpdateState.AwaitingFileSave(uf2Artifact = null, fileName = null).toUpdateProgress()) + assertEquals(null, FirmwareUpdateState.Idle.toUpdateProgress()) + } } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt index cefa45375f..b7355a1e3c 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt @@ -26,6 +26,7 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verify.VerifyMode import dev.mokkery.verifySuspend +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -39,16 +40,18 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.meshtastic.core.common.state.HiddenFeaturesUnlock import org.meshtastic.core.common.state.RadioOperationLock -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.datastore.FirmwareRecoveryDataSource import org.meshtastic.core.datastore.model.PendingFirmwareRecovery import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.model.FirmwareReleaseType import org.meshtastic.core.model.MaintenanceUf2Manifest import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FirmwareReleaseRepository +import org.meshtastic.core.repository.FirmwareUpdateProgress +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.MaintenanceUf2Repository import org.meshtastic.core.repository.NodeRestartTracker import org.meshtastic.core.repository.PlatformAnalytics @@ -56,7 +59,9 @@ import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.firmware_update_battery_low +import org.meshtastic.core.resources.firmware_update_no_device import org.meshtastic.core.resources.firmware_update_unknown_hardware +import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.TestDataFactory @@ -66,6 +71,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertTrue /** @@ -89,6 +95,7 @@ class FirmwareUpdateViewModelTest { private val fileHandler: FirmwareFileHandler = mock(MockMode.autofill) private val firmwareRetriever: FirmwareRetriever = mock(MockMode.autofill) private val analytics: PlatformAnalytics = mock(MockMode.autofill) + private val bluetoothRepository = FakeBluetoothRepository() private lateinit var viewModel: FirmwareUpdateViewModel @@ -158,8 +165,12 @@ class FirmwareUpdateViewModelTest { hiddenFeaturesUnlock, analytics, NodeRestartTracker(TestApplicationCoroutineScope(testDispatcher)), + bluetoothRepository, + firmwareUpdateStatusRepository, ) + private val firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository() + @Test fun `initialization checks for updates and transitions to Ready`() = runTest { advanceUntilIdle() @@ -264,6 +275,30 @@ class FirmwareUpdateViewModelTest { ) } + @Test + fun `a running flash publishes its progress for the service notification`() = runTest { + advanceUntilIdle() + val transferring = CompletableDeferred() + val message = UiText.DynamicString("Writing firmware") + everySuspend { firmwareUpdateManager.startUpdate(any(), any(), any(), any()) } + .calls { + @Suppress("UNCHECKED_CAST") + val updateState = it.args[3] as (FirmwareUpdateState) -> Unit + updateState(FirmwareUpdateState.Updating(ProgressState(message = message, progress = 0.42f))) + transferring.await() + null + } + + viewModel.startUpdate() + runCurrent() + + assertEquals(FirmwareUpdateProgress(message, percent = 42), firmwareUpdateStatusRepository.progress.value) + + transferring.complete(Unit) + advanceUntilIdle() + assertNotEquals(42, firmwareUpdateStatusRepository.progress.value?.percent) + } + @Test fun `startUpdate with wipe factory-resets only after verification succeeds`() = runTest { advanceUntilIdle() @@ -466,6 +501,19 @@ class FirmwareUpdateViewModelTest { assertIs(state.updateMethod) } + @Test + fun `update method is Unknown for a BLE address on hardware without Bluetooth`() = runTest { + every { radioPrefs.devAddr } returns MutableStateFlow("x1234abcd") + bluetoothRepository.isSupported = false + + viewModel = createViewModel() + advanceUntilIdle() + + val state = viewModel.state.value + assertIs(state) + assertIs(state.updateMethod) + } + @Test fun `update method is Wifi for TCP-prefixed address`() = runTest { val hardware = DeviceHardware(hwModel = 1, architecture = "esp32", platformioTarget = "tbeam") @@ -566,6 +614,30 @@ class FirmwareUpdateViewModelTest { assertIs(state.updateMethod) } + @Test + fun `recovery is not offered on hardware without Bluetooth`() = runTest { + every { radioPrefs.devAddr } returns MutableStateFlow(null) + every { firmwareRecoveryDataSource.pending } returns + flowOf( + PendingFirmwareRecovery( + fullAddress = "x1234abcd", + hwModel = 1, + pioEnv = "tbeam", + releaseType = "STABLE", + deviceName = "My Node", + ), + ) + bluetoothRepository.isSupported = false + + viewModel = createViewModel() + advanceUntilIdle() + + val errorState = assertIs(viewModel.state.value) + val error = assertIs(errorState.error) + assertEquals(Res.string.firmware_update_no_device, error.res) + verifySuspend(mode = VerifyMode.not) { firmwareRecoveryDataSource.clear() } + } + @Test fun `nightly recovery record re-asserts the hidden-features unlock`() = runTest { // A NIGHTLY record can only have been written while unlocked; recovery after a process restart diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt index 4dec220ce8..45751855cf 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.kt @@ -69,6 +69,13 @@ class IsValidFirmwareFileTest { assertFalse(isValidFirmwareFile("littlefs-heltec-v3-2.7.17.bin", "heltec-v3", ".bin")) } + @Test + fun `rejects littlefs and bleota images inside a release zip directory`() { + assertFalse(isValidFirmwareFile("esp32s3/littlefs-heltec-v3-2.8.0.abc.bin", "heltec-v3", ".bin")) + assertFalse(isValidFirmwareFile("esp32s3/bleota-heltec-v3-2.8.0.abc.bin", "heltec-v3", ".bin")) + assertTrue(isValidFirmwareFile("esp32s3/firmware-heltec-v3-2.8.0.abc.bin", "heltec-v3", ".bin")) + } + @Test fun `rejects bleota prefix`() { assertFalse(isValidFirmwareFile("bleota-heltec-v3-2.7.17.bin", "heltec-v3", ".bin")) diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt index b452744d67..c3ef7bcaae 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt @@ -23,6 +23,7 @@ import org.meshtastic.core.model.SoftDeviceVariant import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -39,119 +40,120 @@ import kotlin.test.assertTrue */ class UsbMaintenanceGateTest { + private val manifestJson = Json { ignoreUnknownKeys = true } + /** * The real maintenance-UF2 manifest ([api/data/maintenanceUf2.json] in `meshtastic/api`, embedded verbatim), so * this test suite keeps exercising the exact board/digest table that ships, not a hand-trimmed fixture that could * drift from it silently. */ private val testManifest = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "manifestVersion": 1, - "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", - "otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5", - "erase": { - "nrf52": { - "6.1.1": { - "fileName": "nrf_erase2.uf2", - "sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe", - "expectedFirstTargetAddress": 155648 - }, - "7.3.0": { - "fileName": "nrf_erase_sd7_3.uf2", - "sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f", - "expectedFirstTargetAddress": 159744 - } - }, - "nrf52Bootloader": { - "fileName": "meshtastic_factory_erase.uf2", - "sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d", - "expectedFamilyId": 1296388936 - }, - "rp2040": { - "fileName": "pico_erase.uf2", - "sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853" - } + manifestJson.decodeFromString( + """ + { + "manifestVersion": 1, + "otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5", + "otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5", + "erase": { + "nrf52": { + "6.1.1": { + "fileName": "nrf_erase2.uf2", + "sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe", + "expectedFirstTargetAddress": 155648 }, - "otafixByBoardId": { - "HT-n5262": { - "otafixBoardSlug": "heltec_t114", - "sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19" - }, - "MinewSemi-MX25LE01": { - "otafixBoardSlug": "minewsemi_mx25le01", - "sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b" - }, - "TRACKER L1": { - "otafixBoardSlug": "wio_tracker_l1", - "sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e" - }, - "WisBlock-RAK4631-Board": { - "otafixBoardSlug": "wiscore_rak4631_board", - "sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305" - }, - "WisMesh-Tag": { - "otafixBoardSlug": "wismesh_tag", - "sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758" - }, - "nRF52840-SeeedSenseCAPSolarP1-v1": { - "otafixBoardSlug": "sensecap_solar_p1", - "sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5" - }, - "nRF52840-SeeedXiao-v1": { - "otafixBoardSlug": "xiao_nrf52840_ble", - "sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f" - }, - "nRF52840-SeeedXiaoSense-v1": { - "otafixBoardSlug": "xiao_nrf52840_ble_sense", - "sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da" - }, - "nRF52840-T1000-E-v1": { - "otafixBoardSlug": "t1000_e", - "sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4" - }, - "nRF52840-TEcho-v1": { - "otafixBoardSlug": "lilygo_techo", - "sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0" - }, - "nRF52840-ThinkNode-M3-v1": { - "otafixBoardSlug": "thinknode_m3", - "sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708" - }, - "nRF52840-ThinkNodeM1-v1": { - "otafixBoardSlug": "thinknode_m1", - "sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795" - }, - "nRF52840-ThinkNodeM6-v1": { - "otafixBoardSlug": "thinknode_m6", - "sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a" - }, - "nRF52840-promicro": { - "otafixBoardSlug": "promicro_nrf52840", - "sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794" - } - }, - "otafixSupportedTargets": [ - "rak4631", - "rak_wismeshtag", - "t-echo", - "heltec-mesh-node-t114", - "nrf52_promicro_diy_tcxo", - "thinknode_m1", - "thinknode_m3", - "thinknode_m6", - "tracker-t1000-e", - "seeed_wio_tracker_L1", - "seeed_wio_tracker_L1_eink", - "seeed_solar_node", - "seeed_xiao_nrf52840_kit" - ] + "7.3.0": { + "fileName": "nrf_erase_sd7_3.uf2", + "sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f", + "expectedFirstTargetAddress": 159744 + } + }, + "nrf52Bootloader": { + "fileName": "meshtastic_factory_erase.uf2", + "sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d", + "expectedFamilyId": 1296388936 + }, + "rp2040": { + "fileName": "pico_erase.uf2", + "sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853" } - """ - .trimIndent(), - ) + }, + "otafixByBoardId": { + "HT-n5262": { + "otafixBoardSlug": "heltec_t114", + "sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19" + }, + "MinewSemi-MX25LE01": { + "otafixBoardSlug": "minewsemi_mx25le01", + "sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b" + }, + "TRACKER L1": { + "otafixBoardSlug": "wio_tracker_l1", + "sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e" + }, + "WisBlock-RAK4631-Board": { + "otafixBoardSlug": "wiscore_rak4631_board", + "sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305" + }, + "WisMesh-Tag": { + "otafixBoardSlug": "wismesh_tag", + "sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758" + }, + "nRF52840-SeeedSenseCAPSolarP1-v1": { + "otafixBoardSlug": "sensecap_solar_p1", + "sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5" + }, + "nRF52840-SeeedXiao-v1": { + "otafixBoardSlug": "xiao_nrf52840_ble", + "sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f" + }, + "nRF52840-SeeedXiaoSense-v1": { + "otafixBoardSlug": "xiao_nrf52840_ble_sense", + "sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da" + }, + "nRF52840-T1000-E-v1": { + "otafixBoardSlug": "t1000_e", + "sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4" + }, + "nRF52840-TEcho-v1": { + "otafixBoardSlug": "lilygo_techo", + "sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0" + }, + "nRF52840-ThinkNode-M3-v1": { + "otafixBoardSlug": "thinknode_m3", + "sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708" + }, + "nRF52840-ThinkNodeM1-v1": { + "otafixBoardSlug": "thinknode_m1", + "sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795" + }, + "nRF52840-ThinkNodeM6-v1": { + "otafixBoardSlug": "thinknode_m6", + "sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a" + }, + "nRF52840-promicro": { + "otafixBoardSlug": "promicro_nrf52840", + "sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794" + } + }, + "otafixSupportedTargets": [ + "rak4631", + "rak_wismeshtag", + "t-echo", + "heltec-mesh-node-t114", + "nrf52_promicro_diy_tcxo", + "thinknode_m1", + "thinknode_m3", + "thinknode_m6", + "tracker-t1000-e", + "seeed_wio_tracker_L1", + "seeed_wio_tracker_L1_eink", + "seeed_solar_node", + "seeed_xiao_nrf52840_kit" + ] + } + """ + .trimIndent(), + ) private fun nrf( variant: SoftDeviceVariant? = SoftDeviceVariant.S140_6_1_1, @@ -211,20 +213,19 @@ class UsbMaintenanceGateTest { @Test fun `an unsafe erase filename refuses the image instead of throwing`() { val hostile = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "erase": { - "nrf52": { - "6.1.1": { "fileName": "../../etc/passwd", "sha256": "00" } - }, - "rp2040": { "fileName": "sub/dir/pico_erase.uf2", "sha256": "00" } - } - } - """ - .trimIndent(), - ) + manifestJson.decodeFromString( + """ + { + "erase": { + "nrf52": { + "6.1.1": { "fileName": "../../etc/passwd", "sha256": "00" } + }, + "rp2040": { "fileName": "sub/dir/pico_erase.uf2", "sha256": "00" } + } + } + """ + .trimIndent(), + ) assertNull(eraseUf2For(hostile, nrf()), "A traversal fileName must resolve to null, not throw") assertNull(eraseUf2For(hostile, rp2040()), "A separator in fileName must resolve to null, not throw") @@ -237,19 +238,18 @@ class UsbMaintenanceGateTest { @Test fun `an unsafe otafix slug or tag refuses the image instead of throwing`() { val hostile = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "otafixReleaseTag": "../../../evil", - "otafixBase": "https://example.invalid/releases", - "otafixByBoardId": { - "rak4631": { "otafixBoardSlug": "rak4631", "sha256": "00" } - } - } - """ - .trimIndent(), - ) + manifestJson.decodeFromString( + """ + { + "otafixReleaseTag": "../../../evil", + "otafixBase": "https://example.invalid/releases", + "otafixByBoardId": { + "rak4631": { "otafixBoardSlug": "rak4631", "sha256": "00" } + } + } + """ + .trimIndent(), + ) assertNull(otafixUf2ForBoardId(hostile, "rak4631"), "A traversal release tag must resolve to null, not throw") } @@ -868,4 +868,96 @@ class UsbMaintenanceGateTest { assertNull(uf2FamilyId(ByteArray(UF2_BLOCK_BYTES)), "Zeroed bytes carry no UF2 magic") assertNull(uf2FamilyId(ByteArray(32)), "A short payload cannot hold a UF2 block") } + + // ── Installed bootloader version against the release ───────────────────── + + /** + * The `INFO_UF2.TXT` text embedded in the released `update-wismesh_tag_bootloader-0.9.2-OTAFIX2.5_nosd.uf2`, + * extracted from its UF2 payload. The running bootloader appends its `SoftDevice:` line to this at boot. + */ + private val wismeshTagOtafix25Info = + "UF2 Bootloader 0.9.2-OTAFIX2.5 lib/nrfx (v3.14.0) lib/tinyusb (0.21.0-435-g3898a1df4) " + + "lib/uf2 (heads/master)\r\n" + + "Model: WisMesh Tag\r\nBoard-ID: WisMesh-Tag\r\nDate: Sep 8 2026\r\n" + + "Factory-Erase: UF2 family 0x4D455348\r\n" + + private fun volumeFrom(info: String) = MaintenanceVolume( + boardId = assertNotNull(parseUf2BoardId(info)), + softDevice = parseUf2SoftDevice(info), + bootloaderVersion = parseUf2BootloaderVersion(info), + ) + + @Test + fun `bootloader version is the first token of the uf2 bootloader line on every known vintage`() { + assertEquals("0.4.3", parseUf2BootloaderVersion(rak4631StockInfo)) + assertEquals("0.9.2-OTAFIX2.2-BP1.3", parseUf2BootloaderVersion(rak4631OtafixInfo)) + assertEquals("0.9.2-dirty", parseUf2BootloaderVersion(seeedL1Info), "stock Seeed builds carry git's suffix") + assertEquals("0.9.2-OTAFIX2.3-BP1.6", parseUf2BootloaderVersion(rakOtafixEraseInfo)) + assertEquals("0.9.2-OTAFIX2.5", parseUf2BootloaderVersion(wismeshTagOtafix25Info)) + } + + @Test + fun `bootloader version falls back to the ver line and is null when neither line is present`() { + assertEquals("0.4.3", parseUf2BootloaderVersion("Board-ID: WisBlock-RAK4631-Board\r\nVer: 0.4.3\r\n")) + assertNull(parseUf2BootloaderVersion("Board-ID: WisBlock-RAK4631-Board\r\n")) + assertNull(parseUf2BootloaderVersion("UF2 Bootloader \r\nBoard-ID: X\r\n"), "an empty version is no version") + assertNull(parseUf2BootloaderVersion("")) + } + + @Test + fun `the released bootloader reads as current against its own release tag`() { + val manifest = testManifest.copy(otafixReleaseTag = "0.9.2-OTAFIX2.5") + + val review = assertIs(reviewBootloader(manifest, volumeFrom(wismeshTagOtafix25Info))) + + assertEquals(BootloaderVersions(installed = "0.9.2-OTAFIX2.5", available = "0.9.2-OTAFIX2.5"), review.versions) + assertTrue(review.versions.isCurrent) + } + + @Test + fun `any other installed version reads as not current without claiming an order`() { + // testManifest is on BP1.5; BP1.6 is a bench build newer than it, and still only "different". + for (info in listOf(rak4631StockInfo, rak4631OtafixInfo, rakOtafixEraseInfo, wismeshTagOtafix25Info)) { + val review = assertIs(reviewBootloader(testManifest, volumeFrom(info))) + assertFalse(review.versions.isCurrent, "installed ${review.versions.installed}") + assertEquals("0.9.2-OTAFIX2.3-BP1.5", review.versions.available) + } + } + + @Test + fun `a drive reporting no version is never current`() { + val volume = MaintenanceVolume(boardId = "WisMesh-Tag", softDevice = null, bootloaderVersion = null) + + val review = assertIs(reviewBootloader(testManifest, volume)) + + assertNull(review.versions.installed) + assertFalse(BootloaderVersions(installed = null, available = "").isCurrent, "two unknowns are not a match") + assertFalse(review.versions.isCurrent) + } + + @Test + fun `review refuses an unrecognized board id before anything is downloaded`() { + val volume = MaintenanceVolume(boardId = "SomeOtherBoard-v9", softDevice = null, bootloaderVersion = "0.9.2") + + assertEquals( + BootloaderReview.Refused(UsbMaintenanceRefusal.UnknownBoardId), + reviewBootloader(testManifest, volume), + ) + } + + @Test + fun `the gate carries the latest bootloader only where the upgrade is offered`() { + val offered = maintenanceGate(testManifest, nrf(), FirmwareUpdateMethod.Usb, hasRelease = true) + assertEquals("0.9.2-OTAFIX2.3-BP1.5", offered.latestBootloader) + + val unsupported = maintenanceGate(testManifest, nrf(target = "wio-sdk-wm1110"), FirmwareUpdateMethod.Usb, true) + assertFalse(unsupported.showBootloaderUpgrade) + assertNull(unsupported.latestBootloader) + + assertNull(maintenanceGate(testManifest, rp2040(), FirmwareUpdateMethod.Usb, true).latestBootloader) + assertNull( + maintenanceGate(MaintenanceUf2Manifest(), nrf(), FirmwareUpdateMethod.Usb, true).latestBootloader, + "no manifest, no version to show", + ) + } } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt index a91bfe4dac..b8797d51c1 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index 670427e7e0..4dd43701c1 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -26,9 +26,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioController @@ -374,7 +374,6 @@ class Esp32OtaUpdateHandlerTest { } assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") }) assertIs(states.lastOrNull()) - Unit } } @@ -408,7 +407,6 @@ class Esp32OtaUpdateHandlerTest { } assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") }) assertIs(states.lastOrNull()) - Unit } } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpersTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpersTest.kt index 0ac1650e24..ea259834db 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpersTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/FirmwareUpdateHelpersTest.kt @@ -14,12 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_transfer_percent +import org.meshtastic.core.resources.firmware_update_transfer_progress import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame @@ -32,20 +34,26 @@ class FirmwareUpdateHelpersTest { @Test fun `formatTransferProgress omits speed when throughput is zero`() { - assertEquals("50%", formatTransferProgress(progress = 0.5f, totalBytes = 1000, bytesPerSecond = 0)) + assertEquals( + UiText.Resource(Res.string.firmware_update_transfer_percent, 50), + formatTransferProgress(progress = 0.5f, totalBytes = 1000, bytesPerSecond = 0), + ) } @Test fun `formatTransferProgress omits speed when throughput is non-positive`() { - assertEquals("0%", formatTransferProgress(progress = 0f, totalBytes = 1000, bytesPerSecond = -5)) + assertEquals( + UiText.Resource(Res.string.firmware_update_transfer_percent, 0), + formatTransferProgress(progress = 0f, totalBytes = 1000, bytesPerSecond = -5), + ) } @Test - fun `formatTransferProgress includes KiB per second and ETA`() { - // 50% of 2048 bytes (1024 remaining) at 1024 B/s → 1.0 KiB/s, 1s ETA. + fun `formatTransferProgress passes the rate in decimal kilobytes and the ETA in seconds`() { + // 1,000,000 bytes left at 12,600 B/s: 12.60 kB/s (12.3 KiB/s in binary), 79 s to go. assertEquals( - "50% (1.0 KiB/s, ETA: 1s)", - formatTransferProgress(progress = 0.5f, totalBytes = 2048, bytesPerSecond = 1024), + UiText.Resource(Res.string.firmware_update_transfer_progress, 50, "12.60 kB", 79), + formatTransferProgress(progress = 0.5f, totalBytes = 2_000_000, bytesPerSecond = 12_600), ) } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransportTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransportTest.kt index 096b1f7825..d7e1d1fb4b 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransportTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransportTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota import io.ktor.network.selector.SelectorManager diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/DfuHexFormattingTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/DfuHexFormattingTest.kt new file mode 100644 index 0000000000..970ff6c81c --- /dev/null +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/DfuHexFormattingTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware.ota.dfu + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** Pins the zero-padded lowercase hex that DFU error messages carry into logs and bug reports. */ +class DfuHexFormattingTest { + + @Test + fun `unknown legacy status is two padded hex digits`() { + assertEquals("UNKNOWN(0x0a)", LegacyDfuStatus.describe(0x0A)) + assertEquals("UNKNOWN(0xff)", LegacyDfuStatus.describe(0xFF.toByte())) + } + + @Test + fun `unknown extended error is two padded hex digits`() { + assertEquals("Unknown extended error 0x7f", DfuExtendedError.describe(0x7F)) + } + + @Test + fun `protocol error names opcode and result as padded hex`() { + assertEquals( + "DFU protocol error: opcode=0x01 result=0x0b", + DfuException.ProtocolError(opcode = 0x01, resultCode = 0x0B).message, + ) + } + + @Test + fun `checksum mismatch prints both crcs as eight hex digits`() { + assertEquals( + "CRC-32 mismatch: expected 0x00000abc got 0xffffffff", + DfuException.ChecksumMismatch(expected = 0xABC, actual = -1).message, + ) + } +} diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocolTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocolTest.kt index 2b225e2da5..d22f5a8e25 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocolTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuProtocolTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota.dfu import kotlin.test.Test diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuRetryPolicyTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuRetryPolicyTest.kt index f4772d756c..7c84db15a1 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuRetryPolicyTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuRetryPolicyTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota.dfu import kotlinx.coroutines.test.runTest diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt index 5ffcba6b86..f8ecadab25 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber", "LargeClass", "TooManyFunctions") +@file:Suppress("LargeClass", "TooManyFunctions") package org.meshtastic.feature.firmware.ota.dfu @@ -23,6 +23,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation @@ -429,26 +430,28 @@ class LegacyDfuTransportTest { */ var throwExceptionOnControlPointWrites: Boolean = false - override fun hasCharacteristic(c: BleCharacteristic) = delegate.hasCharacteristic(c) + override fun hasCharacteristic(characteristic: BleCharacteristic) = delegate.hasCharacteristic(characteristic) - override fun observe(c: BleCharacteristic): Flow = delegate.observe(c) + override fun observe(characteristic: BleCharacteristic): Flow = delegate.observe(characteristic) - override suspend fun read(c: BleCharacteristic): ByteArray = delegate.read(c) + override suspend fun read(characteristic: BleCharacteristic): ByteArray = delegate.read(characteristic) - override fun preferredWriteType(c: BleCharacteristic): BleWriteType = delegate.preferredWriteType(c) + override fun preferredWriteType(characteristic: BleCharacteristic): BleWriteType = + delegate.preferredWriteType(characteristic) - override suspend fun write(c: BleCharacteristic, data: ByteArray, writeType: BleWriteType) { - if (throwErrorOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) { + override suspend fun write(characteristic: BleCharacteristic, data: ByteArray, writeType: BleWriteType) { + val isControlPoint = characteristic.uuid == LegacyDfuUuids.CONTROL_POINT + if (throwErrorOnControlPointWrites && isControlPoint) { throw AssertionError("Simulated assertion failure during control point write") } - if (throwExceptionOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) { + if (throwExceptionOnControlPointWrites && isControlPoint) { throw RuntimeException("Simulated link failure during control point write") } - if (hangOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) { + if (hangOnControlPointWrites && isControlPoint) { awaitCancellation() } - delegate.write(c, data, writeType) - val response = responder.onWrite(c.uuid, data) ?: return + delegate.write(characteristic, data, writeType) + val response = responder.onWrite(characteristic.uuid, data) ?: return response.forEach { delegate.emitNotification(LegacyDfuUuids.CONTROL_POINT, it) } } } @@ -644,6 +647,7 @@ class LegacyDfuTransportTest { * where the write throws before the watcher processes the emission), while `bleConnection.connectionState.value` * still reads as Disconnected for the write-catch classification. */ + @OptIn(ExperimentalForInheritanceCoroutinesApi::class) private class DisconnectEmissionsSuppressedStateFlow(private val delegate: StateFlow) : StateFlow { override val value: BleConnectionState @@ -971,7 +975,7 @@ class LegacyDfuTransportTest { // parentJob stands in for the caller's scope; cancelling it must propagate through withTimeoutOrNull // (which only swallows its own TimeoutCancellationException, not parent cancellation) and out of abort. val parentJob = Job() - val abortDeferred = async(parentJob) { env.transport.abort() } + val abortDeferred = CoroutineScope(coroutineContext + parentJob).async { env.transport.abort() } // Let abort reach the hanging RESET write. runCurrent() diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransportTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransportTest.kt index ba109c35b5..70da8e6b57 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransportTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransportTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.firmware.ota.dfu import kotlinx.coroutines.CoroutineScope diff --git a/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/BaseFirmwareFileHandler.kt b/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/BaseFirmwareFileHandler.kt new file mode 100644 index 0000000000..118f2e37bd --- /dev/null +++ b/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/BaseFirmwareFileHandler.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware + +import co.touchlab.kermit.Logger +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.head +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.withContext +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.common.util.ioDispatcher +import org.meshtastic.core.model.DeviceHardware +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URI + +/** + * The [FirmwareFileHandler] work the Android and desktop handlers share: HTTP fetches into [tempDir], and firmware + * extraction through the bounded [extractFirmwareEntry]. Subclasses say how a platform URI is opened. + */ +abstract class BaseFirmwareFileHandler(private val client: HttpClient, protected val tempDir: File) : + FirmwareFileHandler { + + /** Opens [uri] for reading, or returns null when the platform cannot resolve it. */ + protected abstract fun openUri(uri: CommonUri): InputStream? + + override fun cleanupAllTemporaryFiles() { + runCatching { + if (tempDir.exists()) { + tempDir.deleteRecursively() + } + tempDir.mkdirs() + } + .onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } } + } + + override suspend fun deleteFile(file: FirmwareArtifact) = withContext(ioDispatcher) { + if (!file.isTemporary) return@withContext + val localFile = file.toLocalFileOrNull() ?: return@withContext + if (localFile.exists()) localFile.delete() + } + + override suspend fun checkUrlExists(url: String): Boolean = withContext(ioDispatcher) { + try { + client.head(url).status.isSuccess() + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.w(e) { "Failed to check URL existence: $url" } + false + } + } + + override suspend fun fetchText(url: String): String? = withContext(ioDispatcher) { + try { + val response = client.get(url) + if (response.status.isSuccess()) response.bodyAsText() else null + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.w(e) { "Failed to fetch text from: $url" } + null + } + } + + override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): FirmwareArtifact? = + withContext(ioDispatcher) { + // fileName can come from a remote manifest, so it must name a file directly inside tempDir. + val targetFile = File(tempDir, fileName) + if (targetFile.canonicalFile.parentFile != tempDir.canonicalFile) { + Logger.w { "Refusing a download name outside the temp directory: $fileName" } + return@withContext null + } + val response = + try { + client.get(url) + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.w(e) { "Download failed for $url" } + return@withContext null + } + + if (!response.status.isSuccess()) { + Logger.w { "Download failed: ${response.status.value} for $url" } + return@withContext null + } + + if (!tempDir.exists()) tempDir.mkdirs() + downloadResponseToFile(response, targetFile, onProgress) + targetFile.toFirmwareArtifact() + } + + override suspend fun extractFirmware( + uri: CommonUri, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String?, + ): FirmwareArtifact? = withContext(ioDispatcher) { + if (hardware.effectiveTarget.isEmpty() && preferredFilename == null) return@withContext null + extractFirmwareEntryOrNull(tempDir, hardware.effectiveTarget, fileExtension, preferredFilename) { + openUri(uri) + } + ?.toFirmwareArtifact() + } + + override suspend fun extractFirmwareFromZip( + zipFile: FirmwareArtifact, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String?, + ): FirmwareArtifact? = withContext(ioDispatcher) { + val localZipFile = zipFile.toLocalFileOrNull() ?: return@withContext null + if (hardware.effectiveTarget.isEmpty() && preferredFilename == null) return@withContext null + extractFirmwareEntryOrNull(tempDir, hardware.effectiveTarget, fileExtension, preferredFilename) { + localZipFile.inputStream() + } + ?.toFirmwareArtifact() + } + + protected fun File.toFirmwareArtifact(): FirmwareArtifact = + FirmwareArtifact(uri = CommonUri.parse(toURI().toString()), fileName = name, isTemporary = true) + + protected fun FirmwareArtifact.toLocalFileOrNull(): File? = uri.toLocalFileOrNull() + + protected fun CommonUri.toLocalFileOrNull(): File? = runCatching { + val parsedUri = URI(toString()) + if (parsedUri.scheme == "file") File(parsedUri) else null + } + .getOrNull() +} + +/** A corrupt archive or one past [extractFirmwareEntry]'s limits yields null, so retrieval moves to its fallbacks. */ +private fun extractFirmwareEntryOrNull( + outputDir: File, + target: String, + fileExtension: String, + preferredFilename: String?, + open: () -> InputStream?, +): File? = try { + open()?.let { extractFirmwareEntry(it, outputDir, target, fileExtension, preferredFilename) } +} catch (e: IOException) { + Logger.w(e) { "Failed to extract firmware" } + null +} catch (e: IllegalArgumentException) { + Logger.w(e) { "Firmware archive refused" } + null +} diff --git a/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/ZipExtraction.kt b/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/ZipExtraction.kt index a1ec44cc6b..c2fa354316 100644 --- a/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/ZipExtraction.kt +++ b/feature/firmware/src/jvmAndroidMain/kotlin/org/meshtastic/feature/firmware/ZipExtraction.kt @@ -17,9 +17,14 @@ package org.meshtastic.feature.firmware import java.io.ByteArrayOutputStream +import java.io.File import java.io.FilterInputStream import java.io.InputStream +import java.io.OutputStream +import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.util.zip.ZipInputStream +import kotlin.io.path.createTempDirectory /** * Ceiling on a firmware archive as delivered. Real Meshtastic release zips are tens of MB, and the whole archive is @@ -35,6 +40,18 @@ internal const val MAX_FIRMWARE_ZIP_BYTES = 128L * 1024 * 1024 */ internal const val MAX_FIRMWARE_UNCOMPRESSED_BYTES = 96L * 1024 * 1024 +/** + * Ceiling on bytes [extractFirmwareEntry] writes to disk across the entries it keeps. A single firmware image is a few + * MB, so this only needs to stop an entry that inflates without end. + */ +internal const val MAX_FIRMWARE_EXTRACTED_BYTES = 96L * 1024 * 1024 + +/** + * Ceiling on bytes [extractFirmwareEntry] inflates from entries it skips. The largest release archive inflates to + * several hundred MB, so this only needs to stop an entry that inflates without end. + */ +internal const val MAX_FIRMWARE_SKIPPED_BYTES = 2L * 1024 * 1024 * 1024 + /** Ceiling on entry count. A release archive holds a few hundred at most. */ internal const val MAX_FIRMWARE_ZIP_ENTRIES = 4096 @@ -71,14 +88,22 @@ private class LimitedInputStream(delegate: InputStream, private val limit: Long) */ internal fun readAtMost(input: InputStream, limit: Long): ByteArray? { val out = ByteArrayOutputStream() + return copyAtMost(input, out, limit)?.let { out.toByteArray() } +} + +/** + * Copies at most [limit] bytes from [input] to [output], returning the count, or null once the source proves longer. + * Stops reading as soon as the limit is passed, so no more than `limit + `[COPY_BUFFER_SIZE] bytes are ever pulled. + */ +internal fun copyAtMost(input: InputStream, output: OutputStream, limit: Long): Long? { val buffer = ByteArray(COPY_BUFFER_SIZE) var total = 0L while (true) { val read = input.read(buffer) - if (read < 0) return out.toByteArray() + if (read < 0) return total total += read if (total > limit) return null - out.write(buffer, 0, read) + output.write(buffer, 0, read) } } @@ -123,3 +148,113 @@ internal fun extractZipEntriesBounded( } return entries } + +/** + * Streams the firmware image for [target] out of the zip in [input] into [outputDir] and returns the written file, or + * null when no entry matches. With [preferredFilename] the first entry of exactly that name wins; otherwise every match + * is staged and the shortest entry name wins, the canonical image when a bundle carries variants. + * + * Bounded by entry count, bytes written and bytes inflated from skipped entries, not by archive size: a release archive + * runs past 200 MB and 250 entries, while a single firmware image is a few MB. Throws [IllegalArgumentException] when a + * bound is exceeded and [java.io.IOException] on a corrupt archive. Matches are staged in a directory of their own and + * only the winner is moved into [outputDir], so a failed call leaves [outputDir] as it found it. + */ +internal fun extractFirmwareEntry( + input: InputStream, + outputDir: File, + target: String, + fileExtension: String, + preferredFilename: String?, + maxEntries: Int = MAX_FIRMWARE_ZIP_ENTRIES, + maxWrittenBytes: Long = MAX_FIRMWARE_EXTRACTED_BYTES, + maxSkippedBytes: Long = MAX_FIRMWARE_SKIPPED_BYTES, +): File? { + outputDir.mkdirs() + val staging = createTempDirectory(outputDir.toPath(), ".extract").toFile() + try { + val limits = ExtractionLimits(maxEntries, maxWrittenBytes, maxSkippedBytes) + val (entryName, staged) = + ZipInputStream(input).use { zip -> + stageFirmwareEntry(zip, staging, target, fileExtension, preferredFilename, limits) + } ?: return null + // Only the last path segment is kept, so an entry name cannot write outside outputDir. + val outFile = File(outputDir, File(entryName.lowercase()).name) + Files.move(staged.toPath(), outFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + return outFile + } finally { + staging.deleteRecursively() + } +} + +private class ExtractionLimits(val maxEntries: Int, val maxWrittenBytes: Long, val maxSkippedBytes: Long) + +/** Discards what it is given, so a skipped entry can be inflated and counted without being kept. */ +private object DiscardOutputStream : OutputStream() { + override fun write(b: Int) = Unit + + override fun write(b: ByteArray, off: Int, len: Int) = Unit +} + +/** + * Writes each matching entry of [zip] to its own file in [staging] and returns the chosen one with its entry name. + * Unmatched entries are inflated against [ExtractionLimits.maxSkippedBytes] on the way past. + */ +private fun stageFirmwareEntry( + zip: ZipInputStream, + staging: File, + target: String, + fileExtension: String, + preferredFilename: String?, + limits: ExtractionLimits, +): Pair? { + val candidates = mutableListOf>() + var writeRemaining = limits.maxWrittenBytes + var skipRemaining = limits.maxSkippedBytes + var entriesSeen = 0 + var entry = zip.nextEntry + while (entry != null) { + if (!entry.isDirectory) { + entriesSeen++ + require(entriesSeen <= limits.maxEntries) { "Firmware archive has more than ${limits.maxEntries} entries" } + } + if (isFirmwareEntryMatch(entry.name, entry.isDirectory, target, fileExtension, preferredFilename)) { + // Named by position, so entries sharing a basename in different directories stay distinct. + val staged = File(staging, candidates.size.toString()) + writeRemaining -= + staged.outputStream().use { copyAtMost(zip, it, writeRemaining) } + ?: throw IllegalArgumentException( + "Firmware entry expands past the ${limits.maxWrittenBytes}-byte limit", + ) + candidates += entry.name to staged + if (preferredFilename != null) return candidates.last() + } else { + skipRemaining -= + copyAtMost(zip, DiscardOutputStream, skipRemaining) + ?: throw IllegalArgumentException( + "Skipped entries inflate past the ${limits.maxSkippedBytes}-byte limit", + ) + } + entry = zip.nextEntry + } + return candidates.minByOrNull { (name, _) -> name.length } +} + +/** + * Whether zip entry [entryName] is the firmware to extract: exactly [preferredFilename] (ignoring its directory and + * case) when one is given, otherwise a firmware image for [target] with [fileExtension]. Directories never match. + */ +internal fun isFirmwareEntryMatch( + entryName: String, + isDirectory: Boolean, + target: String, + fileExtension: String, + preferredFilename: String?, +): Boolean { + if (isDirectory) return false + val name = entryName.lowercase() + return if (preferredFilename != null) { + File(name).name == preferredFilename.lowercase() + } else { + isValidFirmwareFile(name, target.lowercase(), fileExtension) + } +} diff --git a/feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt b/feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt index 37a552606f..7e7e76acff 100644 --- a/feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt +++ b/feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt @@ -16,113 +16,29 @@ */ package org.meshtastic.feature.firmware -import co.touchlab.kermit.Logger import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.request.head -import io.ktor.client.statement.bodyAsText -import io.ktor.http.isSuccess import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.model.DeviceHardware import java.io.File -import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream import java.net.URI import java.nio.file.Files import java.nio.file.StandardCopyOption -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream @Suppress("TooManyFunctions") -@Single -class JvmFirmwareFileHandler(private val client: HttpClient) : FirmwareFileHandler { - private val tempDir = File(System.getProperty("java.io.tmpdir"), "meshtastic/firmware_update") +@Single(binds = [FirmwareFileHandler::class]) +class JvmFirmwareFileHandler(client: HttpClient) : + BaseFirmwareFileHandler(client, File(System.getProperty("java.io.tmpdir"), "meshtastic/firmware_update")) { - override fun cleanupAllTemporaryFiles() { - runCatching { - if (tempDir.exists()) { - tempDir.deleteRecursively() - } - tempDir.mkdirs() - } - .onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } } - } - - override suspend fun checkUrlExists(url: String): Boolean = withContext(ioDispatcher) { - try { - client.head(url).status.isSuccess() - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Failed to check URL existence: $url" } - false - } - } - - override suspend fun fetchText(url: String): String? = withContext(ioDispatcher) { - try { - val response = client.get(url) - if (response.status.isSuccess()) response.bodyAsText() else null - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Failed to fetch text from: $url" } - null - } - } - - override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): FirmwareArtifact? = - withContext(ioDispatcher) { - val response = - try { - client.get(url) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "Download failed for $url" } - return@withContext null - } - - if (!response.status.isSuccess()) { - Logger.w { "Download failed: ${response.status.value} for $url" } - return@withContext null - } - - if (!tempDir.exists()) tempDir.mkdirs() - val targetFile = File(tempDir, fileName) - downloadResponseToFile(response, targetFile, onProgress) - targetFile.toFirmwareArtifact() - } - - override suspend fun extractFirmware( - uri: CommonUri, - hardware: DeviceHardware, - fileExtension: String, - preferredFilename: String?, - ): FirmwareArtifact? = withContext(ioDispatcher) { - val inputFile = uri.toLocalFileOrNull() ?: return@withContext null - extractFromZipFile(inputFile, hardware, fileExtension, preferredFilename) - } - - override suspend fun extractFirmwareFromZip( - zipFile: FirmwareArtifact, - hardware: DeviceHardware, - fileExtension: String, - preferredFilename: String?, - ): FirmwareArtifact? = withContext(ioDispatcher) { - val inputFile = zipFile.toLocalFileOrNull() ?: return@withContext null - extractFromZipFile(inputFile, hardware, fileExtension, preferredFilename) - } + override fun openUri(uri: CommonUri): InputStream? = uri.toLocalFileOrNull()?.inputStream() override suspend fun getFileSize(file: FirmwareArtifact): Long = withContext(ioDispatcher) { file.toLocalFileOrNull()?.takeIf { it.exists() }?.length() ?: 0L } - override suspend fun deleteFile(file: FirmwareArtifact) = withContext(ioDispatcher) { - if (!file.isTemporary) return@withContext - val localFile = file.toLocalFileOrNull() ?: return@withContext - if (localFile.exists()) { - localFile.delete() - } - } - override suspend fun readBytes(artifact: FirmwareArtifact): ByteArray = withContext(ioDispatcher) { val file = artifact.toLocalFileOrNull() ?: throw IOException("Cannot resolve artifact to file: ${artifact.uri}") @@ -154,8 +70,7 @@ class JvmFirmwareFileHandler(private val client: HttpClient) : FirmwareFileHandl /** * Fully expands [artifact] into memory, keyed by entry name. * - * Shares [extractZipEntriesBounded] with the Android handler so the two cannot drift — they previously carried - * independent copies of this loop, and only one of them got bounded. + * Shares [extractZipEntriesBounded] with the Android handler, so both apply the same bounds. */ override suspend fun extractZipEntries(artifact: FirmwareArtifact): Map = withContext(ioDispatcher) { @@ -208,62 +123,4 @@ class JvmFirmwareFileHandler(private val client: HttpClient) : FirmwareFileHandl } .getOrNull() } - - @Suppress("NestedBlockDepth", "ReturnCount") - private fun extractFromZipFile( - zipFile: File, - hardware: DeviceHardware, - fileExtension: String, - preferredFilename: String?, - ): FirmwareArtifact? { - val target = hardware.effectiveTarget - if (target.isEmpty() && preferredFilename == null) return null - - val targetLowerCase = target.lowercase() - val preferredFilenameLower = preferredFilename?.lowercase() - val matchingEntries = mutableListOf>() - - if (!tempDir.exists()) tempDir.mkdirs() - - ZipInputStream(zipFile.inputStream()).use { zipInput -> - var entry = zipInput.nextEntry - while (entry != null) { - val name = entry.name.lowercase() - // File(name).name strips directory components, mitigating ZipSlip attacks - val entryFileName = File(name).name - val isMatch = - if (preferredFilenameLower != null) { - entryFileName == preferredFilenameLower - } else { - !entry.isDirectory && isValidFirmwareFile(name, targetLowerCase, fileExtension) - } - - if (isMatch) { - val outFile = File(tempDir, entryFileName) - FileOutputStream(outFile).use { output -> zipInput.copyTo(output) } - matchingEntries.add(entry to outFile) - - if (preferredFilenameLower != null) { - return outFile.toFirmwareArtifact() - } - } - entry = zipInput.nextEntry - } - } - return matchingEntries.minByOrNull { it.first.name.length }?.second?.toFirmwareArtifact() - } - - private fun isValidFirmwareFile(filename: String, target: String, fileExtension: String): Boolean = - org.meshtastic.feature.firmware.isValidFirmwareFile(filename, target, fileExtension) - - private fun File.toFirmwareArtifact(): FirmwareArtifact = - FirmwareArtifact(uri = CommonUri.parse(toURI().toString()), fileName = name, isTemporary = true) - - private fun FirmwareArtifact.toLocalFileOrNull(): File? = uri.toLocalFileOrNull() - - private fun CommonUri.toLocalFileOrNull(): File? = runCatching { - val parsedUri = URI(toString()) - if (parsedUri.scheme == "file") File(parsedUri) else null - } - .getOrNull() } diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt index 410e5c537e..a71494b2d7 100644 --- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent @@ -44,16 +45,18 @@ import org.meshtastic.core.common.state.HiddenFeaturesUnlock import org.meshtastic.core.common.state.RadioOperation import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.datastore.FirmwareRecoveryDataSource import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.EraseImageEntry +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.MaintenanceUf2EraseSet import org.meshtastic.core.model.MaintenanceUf2Manifest +import org.meshtastic.core.model.OtafixAssetEntry import org.meshtastic.core.model.SoftDeviceVariant import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FirmwareReleaseRepository +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository import org.meshtastic.core.repository.MaintenanceUf2Repository import org.meshtastic.core.repository.NodeRestartTracker import org.meshtastic.core.repository.PlatformAnalytics @@ -61,6 +64,7 @@ import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.firmware_update_extracting +import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.TestDataFactory @@ -97,6 +101,7 @@ class FirmwareUpdateViewModelFileTest { private val fileHandler: FirmwareFileHandler = mock(MockMode.autofill) private val firmwareRetriever: FirmwareRetriever = mock(MockMode.autofill) private val radioOperationLock = RadioOperationLock() + private val firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository() private val analytics: PlatformAnalytics = mock(MockMode.autofill) private lateinit var viewModel: FirmwareUpdateViewModel @@ -180,6 +185,8 @@ class FirmwareUpdateViewModelFileTest { HiddenFeaturesUnlock(), analytics, NodeRestartTracker(TestApplicationCoroutineScope(testDispatcher)), + FakeBluetoothRepository(), + firmwareUpdateStatusRepository, ) private fun firmwareUri(fileName: String): CommonUri = CommonUri.parse("file:///downloads/$fileName") @@ -402,6 +409,7 @@ class FirmwareUpdateViewModelFileTest { val processing = assertIs(viewModel.state.value) val message = assertIs(processing.progressState.message) assertEquals(Res.string.firmware_update_extracting, message.res) + assertNull(firmwareUpdateStatusRepository.progress.value, "checking a picked file is not a running update") allowExtraction.complete(Unit) advanceUntilIdle() @@ -1028,6 +1036,101 @@ class FirmwareUpdateViewModelFileTest { verifySuspend { usbManager.ensureSerialPermission(any()) } } + /** Starts a bootloader upgrade on a RAK4631 whose drive reports [installed], and picks that drive once. */ + private suspend fun TestScope.reviewBootloaderOn(installed: String) { + every { radioPrefs.devAddr } returns MutableStateFlow("s/dev/ttyUSB0") + everySuspend { deviceHardwareRepository.getDeviceHardwareByModel(any(), any(), any()) } returns + Result.success(nrfHardware(SoftDeviceVariant.S140_6_1_1)) + everySuspend { maintenanceUf2Repository.getSnapshot() } returns + testMaintenanceUf2Manifest.copy( + otafixReleaseTag = "0.9.2-OTAFIX2.5", + otafixBase = "https://example.invalid/otafix", + otafixByBoardId = + mapOf("WisBlock-RAK4631-Board" to OtafixAssetEntry("wiscore_rak4631_board", "0".repeat(64))), + otafixSupportedTargets = listOf("rak4631"), + ) + everySuspend { firmwareRetriever.retrieveUsbFirmware(any(), any(), any()) } returns + FirmwareArtifact(uri = CommonUri.parse("file:///tmp/firmware.uf2"), fileName = "firmware.uf2") + everySuspend { firmwareRetriever.retrieveMaintenanceUf2(any(), any()) } returns + FirmwareArtifact(uri = CommonUri.parse("file:///tmp/bootloader.uf2"), fileName = "bootloader.uf2") + everySuspend { fileHandler.isRemovableDestination(any()) } returns true + everySuspend { fileHandler.readSiblingText(any(), any()) } returns + "UF2 Bootloader $installed lib/nrfx (v3.14.0)\r\nBoard-ID: WisBlock-RAK4631-Board\r\n" + + "SoftDevice: S140 6.1.1\r\n" + everySuspend { fileHandler.createDocumentInTree(any(), any(), any()) } returns + CommonUri.parse("content://tree/1234-5678%3A/document/bootloader.uf2") + everySuspend { fileHandler.copyToUri(any(), any()) } returns 1024L + every { usbManager.deviceDetachFlow() } returns flowOf(Unit) + everySuspend { usbManager.serialPortKeys() } returns emptySet() + + viewModel = createViewModel() + advanceUntilIdle() + assertEquals( + "0.9.2-OTAFIX2.5", + assertIs(viewModel.state.value).maintenance.latestBootloader, + ) + viewModel.startBootloaderUpgrade() + runUntilSettled { viewModel.state.value is FirmwareUpdateState.AwaitingFileSave } + assertEquals( + UsbFileSaveStep.BootloaderUpgrade, + assertIs(viewModel.state.value).step, + ) + + viewModel.writeMaintenancePass(CommonUri.parse("content://tree/1234-5678%3A")) + runUntilSettled { viewModel.state.value is FirmwareUpdateState.ReviewingBootloader } + } + + @Test + fun `an up to date bootloader is left alone and the sequence moves on to the firmware`() = runTest { + reviewBootloaderOn(installed = "0.9.2-OTAFIX2.5") + + val review = assertIs(viewModel.state.value) + assertTrue(review.versions.isCurrent) + + viewModel.skipBootloaderUpgrade() + runUntilSettled { + (viewModel.state.value as? FirmwareUpdateState.AwaitingFileSave)?.step == UsbFileSaveStep.Firmware + } + + verifySuspend(mode = VerifyMode.not) { firmwareRetriever.retrieveMaintenanceUf2(any(), any()) } + verifySuspend(mode = VerifyMode.not) { fileHandler.createDocumentInTree(any(), any(), any()) } + assertTrue( + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), + "the device is still in update mode, so the sequence keeps the radio until the firmware is back", + ) + } + + @Test + fun `a different bootloader is written only once the user confirms`() = runTest { + reviewBootloaderOn(installed = "0.9.2-OTAFIX2.3-BP1.5") + + val review = assertIs(viewModel.state.value) + assertEquals("0.9.2-OTAFIX2.3-BP1.5", review.versions.installed) + assertFalse(review.versions.isCurrent) + verifySuspend(mode = VerifyMode.not) { firmwareRetriever.retrieveMaintenanceUf2(any(), any()) } + + viewModel.confirmBootloaderUpgrade() + runUntilSettled { + (viewModel.state.value as? FirmwareUpdateState.AwaitingFileSave)?.step == UsbFileSaveStep.Firmware + } + + verifySuspend(mode = exactly(1)) { firmwareRetriever.retrieveMaintenanceUf2(any(), any()) } + verifySuspend { fileHandler.createDocumentInTree(any(), "bootloader.uf2", any()) } + } + + @Test + fun `cancelling at the bootloader review releases the maintenance lock`() = runTest { + reviewBootloaderOn(installed = "0.9.2-OTAFIX2.5") + + viewModel.cancelUpdate() + advanceUntilIdle() + + assertFalse(radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance)) + viewModel.confirmBootloaderUpgrade() + advanceUntilIdle() + verifySuspend(mode = VerifyMode.not) { firmwareRetriever.retrieveMaintenanceUf2(any(), any()) } + } + @Test fun `a granted USB permission preflight reconnects explicitly instead of waiting on auto-recovery`() = runTest { // Auto-recovery's attach trigger fires while the permission dialog is still up and never retries on diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandlerTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandlerTest.kt new file mode 100644 index 0000000000..c01fa00be4 --- /dev/null +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandlerTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.model.DeviceHardware +import java.io.File +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class JvmFirmwareFileHandlerTest { + private val engine = MockEngine { respond(ByteArray(4) { 1 }) } + private val client = HttpClient(engine) + private val handler = JvmFirmwareFileHandler(client) + private val hardware = DeviceHardware(hwModel = 1, architecture = "esp32", platformioTarget = "heltec-v3") + + @AfterTest fun tearDown() = client.close() + + /** A zip with one more entry than extraction allows, none of them firmware, so nothing is written. */ + private fun overLimitZip(): File { + val file = Files.createTempFile("firmware-handler-test", ".zip").toFile().apply { deleteOnExit() } + ZipOutputStream(file.outputStream()).use { zip -> + repeat(MAX_FIRMWARE_ZIP_ENTRIES + 1) { + zip.putNextEntry(ZipEntry("entry$it")) + zip.closeEntry() + } + } + return file + } + + @Test + fun `a downloaded zip over the extraction limits yields null`() = runTest { + val zip = overLimitZip() + val artifact = FirmwareArtifact(uri = CommonUri.parse(zip.toURI().toString()), fileName = zip.name) + + assertNull(handler.extractFirmwareFromZip(artifact, hardware, ".bin")) + } + + @Test + fun `a picked archive over the extraction limits yields null`() = runTest { + val zip = overLimitZip() + + assertNull(handler.extractFirmware(CommonUri.parse(zip.toURI().toString()), hardware, ".bin")) + } + + @Test + fun `a download name outside the temp directory is refused before any request`() = runTest { + assertNull(handler.downloadFile("https://example.invalid/firmware.bin", "../firmware-handler-test.bin") {}) + assertTrue(engine.requestHistory.isEmpty()) + } + + @Test + fun `a plain download name is fetched into the temp directory`() = runTest { + val artifact = handler.downloadFile("https://example.invalid/firmware.bin", "firmware-handler-test.bin") {} + + assertNotNull(artifact) + handler.deleteFile(artifact) + assertEquals(1, engine.requestHistory.size) + } +} diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ZipExtractionTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ZipExtractionTest.kt index d288f2b719..54bd770464 100644 --- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ZipExtractionTest.kt +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ZipExtractionTest.kt @@ -18,14 +18,19 @@ package org.meshtastic.feature.firmware import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File import java.io.FilterInputStream +import java.io.IOException import java.io.InputStream +import java.nio.file.Files import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.random.Random import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -212,6 +217,160 @@ class ZipExtractionTest { assertEquals(setOf("same.bin"), entries.keys, "duplicates collapse to one key — that is the bug's premise") } + // ---------- extractFirmwareEntry ---------- + + private fun tempDir(): File = Files.createTempDirectory("zip-extraction-test").toFile().apply { deleteOnExit() } + + @Test + fun `the target firmware is written to the output directory`() { + val zip = + zipOf( + "esp32s3/firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(32) { 7 }, + "esp32s3/firmware-tbeam-s3-core-2.8.0.abc.bin" to ByteArray(16) { 1 }, + ) + val out = tempDir() + + val file = extractFirmwareEntry(ByteArrayInputStream(zip), out, "heltec-v3", ".bin", preferredFilename = null) + + assertEquals(File(out, "firmware-heltec-v3-2.8.0.abc.bin"), file) + assertContentEquals(ByteArray(32) { 7 }, file?.readBytes()) + } + + @Test + fun `the shortest matching entry name wins`() { + val zip = + zipOf( + "firmware-heltec-v3-2.8.0.abc-update.bin" to ByteArray(8) { 2 }, + "firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(8) { 1 }, + ) + + val file = extractFirmwareEntry(ByteArrayInputStream(zip), tempDir(), "heltec-v3", ".bin", null) + + assertEquals("firmware-heltec-v3-2.8.0.abc.bin", file?.name) + } + + @Test + fun `a preferred filename is matched by name alone ignoring directory and case`() { + val zip = zipOf("nrf52840/Firmware-RAK4631-2.8.0.abc-ota.zip" to ByteArray(8) { 4 }) + + val file = + extractFirmwareEntry( + ByteArrayInputStream(zip), + tempDir(), + target = "", + fileExtension = ".zip", + preferredFilename = "firmware-rak4631-2.8.0.abc-ota.zip", + ) + + assertEquals("firmware-rak4631-2.8.0.abc-ota.zip", file?.name) + } + + @Test + fun `no match returns null and writes nothing`() { + val zip = zipOf("firmware-tbeam-2.8.0.abc.bin" to ByteArray(8)) + val out = tempDir() + + assertNull(extractFirmwareEntry(ByteArrayInputStream(zip), out, "heltec-v3", ".bin", null)) + assertEquals(0, out.listFiles()?.size) + } + + @Test + fun `an entry past the write budget is refused without reading the rest and leaves no file`() { + val incompressible = Random(seed = 99).nextBytes(256 * 1024) + val zip = zipOf("firmware-heltec-v3-2.8.0.abc.bin" to incompressible) + val counting = CountingStream(ByteArrayInputStream(zip)) + val out = tempDir() + + assertFailsWith { + extractFirmwareEntry(counting, out, "heltec-v3", ".bin", null, maxWrittenBytes = 4096) + } + assertTrue(counting.bytesRead < 64 * 1024, "read ${counting.bytesRead} of ${zip.size} bytes") + assertEquals(0, out.listFiles()?.size) + } + + @Test + fun `extraction refuses too many entries`() { + val zip = zipOf(*Array(20) { "entry$it" to ByteArray(4) }) + + assertFailsWith { + extractFirmwareEntry(ByteArrayInputStream(zip), tempDir(), "heltec-v3", ".bin", null, maxEntries = 10) + } + } + + @Test + fun `a refused archive leaves none of the matches written before the refusal`() { + val zip = + zipOf("firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(8) { 1 }, *Array(20) { "entry$it" to ByteArray(4) }) + val out = tempDir() + + assertFailsWith { + extractFirmwareEntry(ByteArrayInputStream(zip), out, "heltec-v3", ".bin", null, maxEntries = 10) + } + assertEquals(0, out.listFiles()?.size) + } + + @Test + fun `a refused archive leaves an earlier extraction of the same name in place`() { + val out = tempDir() + val earlier = File(out, "firmware-heltec-v3-2.8.0.abc.bin").apply { writeBytes(ByteArray(8) { 9 }) } + val zip = + zipOf("firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(8) { 1 }, *Array(20) { "entry$it" to ByteArray(4) }) + + assertFailsWith { + extractFirmwareEntry(ByteArrayInputStream(zip), out, "heltec-v3", ".bin", null, maxEntries = 10) + } + assertContentEquals(ByteArray(8) { 9 }, earlier.readBytes()) + } + + @Test + fun `matches sharing a basename return the bytes of the chosen entry`() { + val zip = + zipOf( + "a/firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(8) { 1 }, + "longer/firmware-heltec-v3-2.8.0.abc.bin" to ByteArray(8) { 2 }, + ) + + val file = extractFirmwareEntry(ByteArrayInputStream(zip), tempDir(), "heltec-v3", ".bin", null) + + assertContentEquals(ByteArray(8) { 1 }, file?.readBytes()) + } + + @Test + fun `an unmatched entry that inflates past the skip budget is refused`() { + val zip = zipOf("readme.txt" to ByteArray(64 * 1024)) + + assertFailsWith { + extractFirmwareEntry( + ByteArrayInputStream(zip), + tempDir(), + "heltec-v3", + ".bin", + null, + maxSkippedBytes = 4096, + ) + } + } + + @Test + fun `a truncated archive leaves no partial firmware file`() { + val zip = zipOf("firmware-heltec-v3-2.8.0.abc.bin" to Random(seed = 7).nextBytes(64 * 1024)) + val out = tempDir() + + assertFailsWith { + extractFirmwareEntry(ByteArrayInputStream(zip.copyOf(zip.size / 2)), out, "heltec-v3", ".bin", null) + } + assertEquals(0, out.listFiles()?.size) + } + + @Test + fun `entry matching excludes directories and non-firmware images`() { + assertTrue(isFirmwareEntryMatch("esp32/firmware-heltec-v3-2.8.0.bin", false, "heltec-v3", ".bin", null)) + assertFalse(isFirmwareEntryMatch("esp32/firmware-heltec-v3-2.8.0.bin/", true, "heltec-v3", ".bin", null)) + assertFalse(isFirmwareEntryMatch("littlefs-heltec-v3-2.8.0.bin", false, "heltec-v3", ".bin", null)) + assertFalse(isFirmwareEntryMatch("firmware-heltec-v3-2.8.0.factory.bin", false, "heltec-v3", ".bin", null)) + assertFalse(isFirmwareEntryMatch("x/firmware.uf2/", true, "", ".uf2", preferredFilename = "firmware.uf2")) + } + @Test fun `directory entries do not consume the entry budget`() { val out = ByteArrayOutputStream() diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/DownloadProgressDetailTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/DownloadProgressDetailTest.kt new file mode 100644 index 0000000000..71546fc3bd --- /dev/null +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/DownloadProgressDetailTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware.ota + +import dev.mokkery.MockMode +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.FirmwareRelease +import org.meshtastic.core.repository.FirmwareUpdateStatusRepository +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.UiText +import org.meshtastic.core.resources.firmware_update_transfer_percent +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.testing.FakeRadioController +import org.meshtastic.feature.firmware.FirmwareArtifact +import org.meshtastic.feature.firmware.FirmwareFileHandler +import org.meshtastic.feature.firmware.FirmwareRetriever +import org.meshtastic.feature.firmware.FirmwareUpdateState +import org.meshtastic.feature.firmware.ota.dfu.SecureDfuHandler +import kotlin.test.Test +import kotlin.test.assertEquals + +/** The download step of the ESP32 and Nordic handlers, which resolves compose resources and so runs on the JVM. */ +class DownloadProgressDetailTest { + + private val release = FirmwareRelease(id = "v2.7.17", title = "test") + private val fileHandler: FirmwareFileHandler = mock(MockMode.autofill) + private val dispatchers = + CoroutineDispatchers( + io = Dispatchers.Unconfined, + main = Dispatchers.Unconfined, + default = Dispatchers.Unconfined, + ) + + /** Reports a quarter of the download, then finds no file, so each handler stops right after its download step. */ + private val retriever = + object : FirmwareRetriever(fileHandler) { + override suspend fun retrieveEsp32Firmware( + release: FirmwareRelease, + hardware: DeviceHardware, + onProgress: (Float) -> Unit, + ): FirmwareArtifact? { + onProgress(QUARTER) + return null + } + + override suspend fun retrieveOtaFirmware( + release: FirmwareRelease, + hardware: DeviceHardware, + onProgress: (Float) -> Unit, + ): FirmwareArtifact? { + onProgress(QUARTER) + return null + } + } + + @Test + fun `ESP32 download progress is a translated percentage`() = runTest { + val handler = + Esp32OtaUpdateHandler( + firmwareRetriever = retriever, + firmwareFileHandler = fileHandler, + radioController = FakeRadioController(), + nodeRepository = FakeNodeRepository(), + firmwareUpdateStatusRepository = FirmwareUpdateStatusRepository(), + environment = DefaultEsp32OtaUpdateEnvironment(), + bleScanner = mock(MockMode.autofill), + bleConnectionFactory = mock(MockMode.autofill), + dispatchers = dispatchers, + ) + val states = mutableListOf() + + handler.startUpdate(release, ESP32, target = BLE_ADDRESS, updateState = states::add, firmwareUri = null) + + assertEquals(listOf(percentDetail(25)), states.downloadDetails()) + } + + @Test + fun `Nordic DFU download progress is a translated percentage`() = runTest { + val handler = + SecureDfuHandler( + firmwareRetriever = retriever, + firmwareFileHandler = fileHandler, + radioController = FakeRadioController(), + bleScanner = mock(MockMode.autofill), + bleConnectionFactory = mock(MockMode.autofill), + dispatchers = dispatchers, + ) + val states = mutableListOf() + + handler.startUpdate(release, NRF52, target = BLE_ADDRESS, updateState = states::add, firmwareUri = null) + + assertEquals(listOf(percentDetail(25)), states.downloadDetails()) + } + + private fun List.downloadDetails() = + filterIsInstance().mapNotNull { it.progressState.details } + + private fun percentDetail(percent: Int) = UiText.Resource(Res.string.firmware_update_transfer_percent, percent) + + private companion object { + const val QUARTER = 0.25f + const val BLE_ADDRESS = "AA:BB:CC:DD:EE:FF" + val ESP32 = DeviceHardware(hwModelSlug = "HELTEC_V3", platformioTarget = "heltec-v3", architecture = "esp32-s3") + val NRF52 = DeviceHardware(hwModelSlug = "RAK4631", platformioTarget = "rak4631", architecture = "nrf52840") + } +} diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/TransferProgressTextTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/TransferProgressTextTest.kt new file mode 100644 index 0000000000..66fb1f911a --- /dev/null +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/ota/TransferProgressTextTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware.ota + +import kotlinx.coroutines.test.runTest +import java.util.Locale +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** The progress text as the screen resolves it from string resources. */ +class TransferProgressTextTest { + + private lateinit var originalLocale: Locale + + @BeforeTest + fun setUp() { + originalLocale = Locale.getDefault() + } + + @AfterTest + fun tearDown() { + Locale.setDefault(originalLocale) + } + + @Test + fun `English reads the rate in decimal kilobytes per second`() = runTest { + Locale.setDefault(Locale.US) + + assertEquals("50% (12.60 kB/s, ETA: 79s)", sample().resolve()) + } + + @Test + fun `a comma-decimal locale gets its own separator in the rate`() = runTest { + Locale.setDefault(Locale.GERMANY) + val text = sample().resolve() + + assertTrue("12,60 kB" in text, text) + } + + @Test + fun `before a throughput sample only the percentage shows`() = runTest { + Locale.setDefault(Locale.US) + + assertEquals("50%", formatTransferProgress(progress = 0.5f, totalBytes = 1000, bytesPerSecond = 0).resolve()) + } + + @Test + fun `download progress reads as the bare percentage`() = runTest { + Locale.setDefault(Locale.US) + + assertEquals("25%", formatTransferPercent(progress = 0.25f).resolve()) + } + + private fun sample() = formatTransferProgress(progress = 0.5f, totalBytes = 2_000_000, bytesPerSecond = 12_600) +} diff --git a/feature/intro/README.md b/feature/intro/README.md index bcaa057e88..5db15fa269 100644 --- a/feature/intro/README.md +++ b/feature/intro/README.md @@ -26,9 +26,6 @@ Dedicated screens for explaining and requesting specific permissions: graph TB :feature:intro[intro]:::kmp-feature :feature:intro -.-> :core:service - :feature:intro -.-> :core:common - :feature:intro -.-> :core:model - :feature:intro -.-> :core:repository :feature:intro -.-> :core:ui :feature:intro -.-> :core:resources :feature:intro -.-> :core:testing diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index d81209a888..b1078127bf 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -25,9 +25,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(projects.core.common) - implementation(projects.core.model) - implementation(projects.core.repository) implementation(projects.core.ui) implementation(projects.core.resources) } diff --git a/feature/intro/detekt-baseline.xml b/feature/intro/detekt-baseline.xml index 23792d7491..8b0920ebbd 100644 --- a/feature/intro/detekt-baseline.xml +++ b/feature/intro/detekt-baseline.xml @@ -2,8 +2,6 @@ - ComposableParamOrder:PermissionScreenLayout.kt:@Composable internal fun PermissionScreenLayout ParameterNaming:WelcomeScreen.kt:onGetStarted: () -> Unit - PreviewPublic:WelcomeScreen.kt:@Preview @Composable fun WelcomeScreenPreview diff --git a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AndroidIntroPermissions.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AndroidIntroPermissions.kt index 66dfaf668e..b41520b638 100644 --- a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AndroidIntroPermissions.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AndroidIntroPermissions.kt @@ -30,4 +30,5 @@ internal class AndroidIntroPermissions( override val location: PermissionUiState, override val notification: PermissionUiState?, override val bluetoothRequiresLocation: Boolean, + override val bluetoothSupported: Boolean, ) : IntroPermissions diff --git a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt index 6b82223dfd..fa5c8bc261 100644 --- a/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import org.meshtastic.core.ui.component.MeshtasticNavDisplay +import org.meshtastic.core.ui.util.isBluetoothSupported import org.meshtastic.core.ui.util.rememberBluetoothPermissionState import org.meshtastic.core.ui.util.rememberLocationPermissionState import org.meshtastic.core.ui.util.rememberNotificationPermissionState @@ -59,6 +60,7 @@ fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel) { location = locationPermissionState, notification = notificationPermissionState, bluetoothRequiresLocation = bluetoothRequiresLocation, + bluetoothSupported = isBluetoothSupported(), ) val settingsNavigator = remember(context) { AndroidIntroSettingsNavigator(context) } val backStack = rememberNavBackStack(Welcome) diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt index 329311c6fd..2927f2003f 100644 --- a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt @@ -33,8 +33,9 @@ internal fun EntryProviderScope.introGraph( current: NavKey, permissionsGranted: Boolean = true, bluetoothRequiresLocation: Boolean = false, + bluetoothSupported: Boolean = true, ) { - val next = viewModel.getNextKey(current, permissionsGranted, bluetoothRequiresLocation) + val next = viewModel.getNextKey(current, permissionsGranted, bluetoothRequiresLocation, bluetoothSupported) if (next != null) { backStack.add(next) } else { @@ -54,7 +55,10 @@ internal fun EntryProviderScope.introGraph( } } - entry { WelcomeScreen(onGetStarted = { navigateToNext(Welcome) }) } + entry { + val permissions = LocalIntroPermissions.current + WelcomeScreen(onGetStarted = { navigateToNext(Welcome, bluetoothSupported = permissions.bluetoothSupported) }) + } entry { val permissions = LocalIntroPermissions.current diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroPermissions.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroPermissions.kt index 89300903b0..ba0b6e2660 100644 --- a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroPermissions.kt +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroPermissions.kt @@ -42,6 +42,11 @@ interface IntroPermissions { * rather than naming a permission the user will never see. */ val bluetoothRequiresLocation: Boolean + + /** + * False on hardware with no Bluetooth LE (e.g. Android XR), where there is nothing for the Bluetooth screen to ask. + */ + val bluetoothSupported: Boolean } /** Provides platform-specific permission states to the intro nav graph. */ diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt index 539ba6d121..e7e3429a18 100644 --- a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt @@ -34,13 +34,15 @@ class IntroViewModel : ViewModel() { * in a row — and a user who declines both has spent both of Android's allowed denials before ever seeing the app, * landing on USER_FIXED with no dialog available again. The Bluetooth screen covers both uses on those releases, * so the second ask is dropped rather than duplicated. + * @param bluetoothSupported false on hardware with no Bluetooth LE, where the Bluetooth screen is skipped. */ fun getNextKey( currentKey: NavKey, allPermissionsGranted: Boolean, bluetoothRequiresLocation: Boolean = false, + bluetoothSupported: Boolean = true, ): NavKey? = when (currentKey) { - is Welcome -> Bluetooth + is Welcome -> if (bluetoothSupported) Bluetooth else Location is Bluetooth -> if (bluetoothRequiresLocation) Notifications else Location is Location -> Notifications is Notifications -> if (allPermissionsGranted) CriticalAlerts else null diff --git a/feature/intro/src/commonTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt b/feature/intro/src/commonTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt index 19a91ced9c..edc6915f93 100644 --- a/feature/intro/src/commonTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt +++ b/feature/intro/src/commonTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt @@ -41,6 +41,12 @@ class IntroViewModelTest { assertEquals(Bluetooth, next) } + @Test + fun testWelcomeSkipsBluetoothOnHardwareWithoutIt() { + val next = viewModel.getNextKey(Welcome, allPermissionsGranted = false, bluetoothSupported = false) + assertEquals(Location, next) + } + @Test fun testBluetoothNavigatesToLocation() { val next = viewModel.getNextKey(Bluetooth, allPermissionsGranted = false) diff --git a/feature/intro/src/jvmMain/kotlin/org/meshtastic/feature/intro/JvmIntroDefaults.kt b/feature/intro/src/jvmMain/kotlin/org/meshtastic/feature/intro/JvmIntroDefaults.kt index 92d34dafb5..7ba084f9ca 100644 --- a/feature/intro/src/jvmMain/kotlin/org/meshtastic/feature/intro/JvmIntroDefaults.kt +++ b/feature/intro/src/jvmMain/kotlin/org/meshtastic/feature/intro/JvmIntroDefaults.kt @@ -27,6 +27,7 @@ internal object JvmIntroPermissions : IntroPermissions { override val location: PermissionUiState = granted override val notification: PermissionUiState = granted override val bluetoothRequiresLocation: Boolean = false + override val bluetoothSupported: Boolean = true } /** JVM/Desktop stub: settings navigation is a no-op. */ diff --git a/feature/map-maplibre/build.gradle.kts b/feature/map-maplibre/build.gradle.kts index abfcf161de..8ebc8d9d10 100644 --- a/feature/map-maplibre/build.gradle.kts +++ b/feature/map-maplibre/build.gradle.kts @@ -37,14 +37,9 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.di) implementation(projects.core.model) - implementation(projects.core.navigation) - implementation(projects.core.prefs) implementation(projects.core.repository) implementation(projects.core.resources) - implementation(projects.core.service) implementation(projects.core.ui) implementation(projects.feature.map) // Offline terrain math (elevation decode, contour generation, zoom-banded intervals) and storage diff --git a/feature/map-maplibre/detekt-baseline.xml b/feature/map-maplibre/detekt-baseline.xml new file mode 100644 index 0000000000..3a3c0bec73 --- /dev/null +++ b/feature/map-maplibre/detekt-baseline.xml @@ -0,0 +1,8 @@ + + + + + LongParameterList:WaypointDialogs.kt:WaypointEditing + UnnecessaryLaunchedEffect:MapLibreMapViewProvider.kt:LaunchedEffect + + diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt index b964d4c7ef..ead657e24f 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt @@ -50,7 +50,7 @@ internal fun rememberRestoredCamera(): RestoredCamera? { val mapPrefs: MapPrefs = koinInject() var restored by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { + LaunchedEffect(mapPrefs) { val saved = mapPrefs.awaitCameraPosition() restored = RestoredCamera( diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt index f06fefda9e..a07b514e99 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt @@ -17,10 +17,12 @@ package org.meshtastic.feature.map.maplibre import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.LayoutDirection import org.maplibre.compose.camera.CameraAnimation import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.CameraUpdate import org.maplibre.compose.map.MapState +import org.maplibre.compose.util.DpPadding import org.maplibre.spatialk.geojson.BoundingBox /** @@ -35,7 +37,7 @@ import org.maplibre.spatialk.geojson.BoundingBox internal suspend fun MapState.zoomBy(delta: Double, range: ClosedFloatingPointRange) { val target = (cameraPosition.zoom + delta).coerceIn(range.start.toDouble(), range.endInclusive.toDouble()) if (target != cameraPosition.zoom) { - animateCameraPosition(cameraPosition.copy(zoom = target), animation = CameraAnimation.Ease()) + animateCamera(CameraUpdate(zoom = target), CameraAnimation.Ease()) } } @@ -46,11 +48,22 @@ internal const val ZOOM_STEP = 1.0 * Frames [bounds] without zooming past [FRAME_MAX_ZOOM]: the fit is computed first and capped before the camera moves, * so nodes standing metres apart open on their surroundings rather than on empty tiles. Eased, not the default flight. */ -internal suspend fun MapState.frameBounds(bounds: BoundingBox, padding: PaddingValues = PaddingValues(0.dp)) { - val fitted = cameraForBounds(bounds, padding = padding) - animateCameraPosition(fitted.cappedTo(FRAME_MAX_ZOOM), CameraAnimation.Ease()) +internal suspend fun MapState.frameBounds(bounds: BoundingBox, padding: DpPadding = DpPadding.Zero) { + val fitted = cameraForBounds(bounds, fitPadding = padding) + animateCamera(fitted.cappedTo(FRAME_MAX_ZOOM).toCameraUpdate(), CameraAnimation.Ease()) } +/** + * These insets as the physical edges MapLibre takes. Resolving start and end here keeps the wide trailing inset on the + * zoom pair's side in RTL too. + */ +internal fun PaddingValues.toDpPadding(layoutDirection: LayoutDirection) = DpPadding( + left = calculateLeftPadding(layoutDirection), + top = calculateTopPadding(), + right = calculateRightPadding(layoutDirection), + bottom = calculateBottomPadding(), +) + /** This position, zoomed out to [maxZoom] if it is tighter than that. */ internal fun CameraPosition.cappedTo(maxZoom: Double): CameraPosition = if (zoom > maxZoom) copy(zoom = maxZoom) else this diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapGeometry.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapGeometry.kt index 1777f78d3c..8681876d61 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapGeometry.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapGeometry.kt @@ -21,12 +21,9 @@ import org.maplibre.spatialk.geojson.Position import org.meshtastic.core.model.Node import org.meshtastic.feature.map.MapBounds import org.meshtastic.feature.map.MapPoint -import kotlin.math.PI -import kotlin.math.cos +import org.meshtastic.feature.map.terrain.TerrainTileMath import kotlin.math.floor -import kotlin.math.ln import kotlin.math.pow -import kotlin.math.tan /** [MapBounds.aroundNodes] as the box MapLibre wants. */ fun nodesBoundingBox(nodes: List): BoundingBox? = MapBounds.aroundNodes(nodes)?.toBoundingBox() @@ -133,18 +130,12 @@ private fun Pair.cell(radiusPx: Int): Pair = /** This node's position in Web Mercator pixels for a world [worldSize] pixels across. */ private fun Node.worldPixel(worldSize: Double): Pair { - val x = (longitude + HALF_TURN) / FULL_TURN * worldSize - val latitudeRadians = latitude * PI / STRAIGHT_ANGLE - val mercatorY = ln(tan(latitudeRadians) + 1.0 / cos(latitudeRadians)) - val y = (1.0 - mercatorY / PI) / 2.0 * worldSize - return x to y + val (x, y) = TerrainTileMath.worldFraction(latitude, longitude) + return x * worldSize to y * worldSize } /** MapLibre's tile size in pixels, which is the space clustering measures its radius in. */ private const val TILE_SIZE = 512.0 -private const val HALF_TURN = 180.0 -private const val FULL_TURN = 360.0 -private const val STRAIGHT_ANGLE = 180.0 /** * The nodes inside [bounds], or all of them when there are none to compare against. diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt index 86475ac01c..882342c469 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,6 +37,7 @@ import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import org.maplibre.compose.camera.CameraAnimation import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.CameraUpdate import org.maplibre.compose.location.BearingUpdate import org.maplibre.compose.location.LocationPermission import org.maplibre.compose.location.LocationState @@ -231,7 +231,7 @@ private fun rememberMapScreenMapState( basemap = basemap, initialCameraPosition = restored.position ?: CameraPosition(), overlays = screen.overlays, - layerOpacity = koinInject().opacity.collectAsState().value, + layerOpacity = koinInject().opacity.collectAsStateWithLifecycle().value, customLayers = customLayers, onClusterMembers = { screen.clusterMembers = it }, onWaypointClick = { screen.infoWaypointId = it }, @@ -343,12 +343,7 @@ private fun SitePlannerSlot( nodeNum = nodeNum, mapCenter = { mapState.cameraPosition.target }, moveTo = { target -> - scope.launch { - mapState.animateCameraPosition( - mapState.cameraPosition.copy(target = target), - animation = CameraAnimation.Ease(), - ) - } + scope.launch { mapState.animateCamera(CameraUpdate(target = target), CameraAnimation.Ease()) } }, onDismiss = onDismiss, ), @@ -395,12 +390,7 @@ private fun BoxScope.MapToolbar( if (location.following) { location.onToggleBearingLock() } else { - scope.launch { - mapState.animateCameraPosition( - mapState.cameraPosition.copy(bearing = 0.0), - animation = CameraAnimation.Ease(), - ) - } + scope.launch { mapState.animateCamera(CameraUpdate(bearing = 0.0), CameraAnimation.Ease()) } } }, filterDropdownContent = { diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt index 5267205a96..68bd482664 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.feature.map.maplibre +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,31 +28,38 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonObject import org.maplibre.compose.camera.CameraAnimation import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.CameraUpdate import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.interaction.ClickResult import org.maplibre.compose.interaction.MapInteractions import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.location.BearingUpdate -import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationState import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.updateCamera import org.maplibre.compose.map.CameraConstraints import org.maplibre.compose.map.LocalMapState import org.maplibre.compose.map.LocalViewport +import org.maplibre.compose.map.MapEvent import org.maplibre.compose.map.MapState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.rememberMapState -import org.maplibre.compose.material3.LocationPuckDefaults +import org.maplibre.compose.material3.LocationIndicatorDefaults +import org.maplibre.compose.material3.LocationIndicatorLayer import org.maplibre.compose.overlay.include import org.maplibre.compose.util.MaplibreComposable import org.maplibre.spatialk.geojson.BoundingBox @@ -64,6 +72,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.feature.map.BaseMapViewModel import org.meshtastic.feature.map.MapNodePolicy import org.meshtastic.feature.map.component.MapEngineUnavailable +import org.meshtastic.feature.map.component.MeshMapFitPadding import org.meshtastic.feature.map.maplibre.component.MeshMapOrnaments import org.meshtastic.feature.map.maplibre.geojson.ClusterMember import org.meshtastic.feature.map.maplibre.geojson.rememberFeatureSource @@ -81,6 +90,8 @@ import org.meshtastic.feature.map.maplibre.style.MapOverlay import org.meshtastic.feature.map.maplibre.style.toBaseStyle import org.meshtastic.feature.map.maplibre.style.zoomRange import kotlin.math.floor +import kotlin.time.Duration.Companion.seconds +import org.maplibre.compose.layers.LocationIndicatorDefaults as CoreLocationIndicatorDefaults /** * Everything the mesh map draws, as one [MapState]. @@ -216,10 +227,18 @@ fun MeshMap( /** Called with the tapped position. Used to collect the two corners of a waypoint's geofence bounding box. */ onMapClick: (Position) -> Unit = {}, ) { - // No engine on this device means presenting a map is the UnsatisfiedLinkError crash in #7001. Guarded here and - // not at the state, which is pure Kotlin: the native library is loaded by the map *view*. + // No engine on this device means presenting a map is the UnsatisfiedLinkError crash in #7001. Covers the view + // only: `rememberMapState` builds the native runtime itself, so a runtime that cannot come up at all has already + // thrown by the time this is reached. if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) + // The store-screenshot capture waits for this tag instead of a fixed delay. + LaunchedEffect(mapState) { + // Idle can arrive before the first render session; only an idle after a drawn frame means tiles are on screen. + mapState.events.dropWhile { it !is MapEvent.FrameRendered }.filterIsInstance().first() + Logger.withTag("MapDrawn").d { "tiles drawn" } + } + val zoomRange = basemap.zoomRange() MaplibreMap( modifier = modifier, @@ -279,9 +298,9 @@ private fun MeshMapNodeLayers( val current = mapState.cameraPosition // A cluster that cannot report an expansion zoom answers with a sentinel (0 on // Android and desktop, -1 on iOS), so clamp — never zoom out on a tap. - mapState.animateCameraPosition( - current.copy(target = centre, zoom = maxOf(expansionZoom, current.zoom)), - animation = CameraAnimation.Ease(), + mapState.animateCamera( + CameraUpdate(target = centre, zoom = maxOf(expansionZoom, current.zoom)), + CameraAnimation.Ease(), ) } }, @@ -306,6 +325,7 @@ private fun FrameOnce(enabled: Boolean, nodes: List, mapState: MapState) { // packet re-frames over them) one of the two failure modes was always reachable. Nothing here restarts on // node changes now, so the latch and the fit cannot come apart. val currentNodes by rememberUpdatedState(nodes) + val fitPadding = MeshMapFitPadding.toDpPadding(LocalLayoutDirection.current) // An effect, not composition-body work: a launch from composition fires even if the composition is // abandoned, while its state write is rolled back — a camera jump with no framing recorded. Fitting before // the map reports a viewport silently lands on a default, hence the gate. @@ -314,7 +334,7 @@ private fun FrameOnce(enabled: Boolean, nodes: List, mapState: MapState) { // Waits for the first node set that has anything to frame; a mesh still filling in reports none. val box = snapshotFlow { nodesBoundingBox(currentNodes) }.filterNotNull().first() hasFramed = true - mapState.frameBounds(box) + mapState.frameBounds(box, padding = fitPadding) } } @@ -341,12 +361,41 @@ private fun FollowUserLocation( @Composable @MaplibreComposable private fun UserLocationPuck(locationState: LocationState?, visible: Boolean) { - if (locationState == null || !visible) return + if (locationState == null) return - // The state overload, which resolves the latest measurement and its most accurate bearing itself. - LocationPuck(idPrefix = "user-location", locationState = locationState, colors = LocationPuckDefaults.colors()) + // The indicator has no notion of age, so a fix that stops arriving is greyed and loses its accuracy ring here, + // or an old position reads as a current one. + val mark = locationState.lastLocationMeasurementMark + var stale by remember(mark) { mutableStateOf(mark != null && mark.elapsedNow() > STALE_LOCATION_AFTER) } + LaunchedEffect(mark) { + if (mark == null || stale) return@LaunchedEffect + delay(STALE_LOCATION_AFTER - mark.elapsedNow()) + stale = true + } + val colors = MaterialTheme.colorScheme + + // The state overload, which resolves the latest measurement and its most accurate bearing itself. Hidden rather + // than unmounted when tracking stops, since layer additions are queued and a quick toggle could lose the re-add. + LocationIndicatorLayer( + id = "user-location", + locationState = locationState, + visible = visible, + accuracyRadiusColor = if (stale) Color.Transparent else colors.primary.copy(alpha = ACCURACY_FILL_ALPHA), + accuracyRadiusBorderColor = if (stale) Color.Transparent else colors.primary, + topImage = + if (stale) { + CoreLocationIndicatorDefaults.topImage(colors.surfaceDim, colors.onPrimary) + } else { + LocationIndicatorDefaults.topImage() + }, + ) } +/** How long a location fix counts as current, the threshold the library's own puck used before 0.18. */ +private val STALE_LOCATION_AFTER = 30.seconds + +private const val ACCURACY_FILL_ALPHA = 0.15f + /** * The first corner tapped while authoring a geofence box. * diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt index 3791849329..8e5e85598c 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt @@ -92,6 +92,7 @@ fun MapLibreNodeTrackMap( modifier: Modifier = Modifier, selectedPositionTime: Int? = null, onPositionSelect: ((Int) -> Unit)? = null, + showAttribution: Boolean = true, customBasemaps: @Composable () -> List = { customRasterBasemaps() }, ) { // Oldest first, as the Google flavor sorts its own track. Everything downstream reads order as age: the gradient @@ -141,7 +142,7 @@ fun MapLibreNodeTrackMap( // The map and its toolbar stay up even when the filter empties the track — otherwise the control that emptied it // disappears along with the points, leaving no way back. Box(modifier = modifier) { - SecondaryMapSurface(mapState = mapState, basemaps = basemaps) + SecondaryMapSurface(mapState = mapState, basemaps = basemaps, showAttribution = showAttribution) SecondaryMapChrome( mapState = mapState, basemaps = basemaps, @@ -168,8 +169,8 @@ fun MapLibreNodeTrackMap( ?.takeIf { selected -> points.any { it.second == selected } } ?.let { selected -> positions.firstOrNull { it.time == selected } }, displayUnits = displayUnits, - // Clear of the logo and attribution along the bottom edge, which the styles are licensed on condition of - // showing. See MeshMapOrnaments. + // Clear of the wordmark and attribution button along the bottom edge, which every map shows. + // See MeshMapOrnaments. modifier = Modifier.align(Alignment.BottomStart) .padding(start = CARD_INSET.dp, end = CARD_INSET.dp, bottom = ORNAMENT_CLEARANCE.dp), @@ -188,10 +189,10 @@ private fun ProtoPosition.toTrackPoint(): TrackPoint? { return if (latitude == 0.0 && longitude == 0.0) { null } else { - // A missing time becomes 0 deliberately: RTC-less nodes report positions with no usable time, and - // dropping those points would erase real tracks. Zero sorts them oldest; the card's guard keeps an - // unselected map from matching them. - GeoPosition(longitude = longitude, latitude = latitude) to (time ?: 0) + // A missing time arrives as 0 and is kept deliberately: RTC-less nodes report positions with no usable + // time, and dropping those points would erase real tracks. Zero sorts them oldest; the card's guard keeps + // an unselected map from matching them. + GeoPosition(longitude = longitude, latitude = latitude) to time } } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt index 3527c4ba65..0cae86f855 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.interaction.MapInteractions @@ -34,15 +35,18 @@ import org.maplibre.compose.map.MapState import org.maplibre.compose.map.MapUiOptions import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.rememberMapState +import org.maplibre.compose.overlay.include import org.maplibre.spatialk.geojson.BoundingBox import org.meshtastic.feature.map.component.MapEngineUnavailable import org.meshtastic.feature.map.maplibre.component.BasemapSelection +import org.meshtastic.feature.map.maplibre.component.CollapsedAttributionOrnaments import org.meshtastic.feature.map.maplibre.component.MapZoom import org.meshtastic.feature.map.maplibre.component.SecondaryMapControls import org.meshtastic.feature.map.maplibre.layers.RasterBasemapLayer import org.meshtastic.feature.map.maplibre.style.Basemap import org.meshtastic.feature.map.maplibre.style.toBaseStyle import org.meshtastic.feature.map.maplibre.style.zoomRange +import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay /** * The map state the maps outside the main one all share. @@ -82,6 +86,9 @@ internal fun rememberSecondaryMapState( * * [basemaps] must be the same selection that state was built with: it supplies the zoom range the camera is held to, * and a different value here would clamp the camera to a range the loaded style cannot serve. + * + * [showAttribution] picks between the credit open and the credit collapsed; both keep the wordmark and the button that + * reveals it. See [CollapsedAttributionOrnaments]. */ @Composable internal fun SecondaryMapSurface( @@ -90,9 +97,10 @@ internal fun SecondaryMapSurface( modifier: Modifier = Modifier.fillMaxSize(), interactions: MapInteractions = SecondaryMapInteractions, uiOptions: MapUiOptions = MapUiOptions.Standard, + showAttribution: Boolean = true, ) { - // Same guard as MeshMap, and in the same place: the state is pure Kotlin, the map view is what loads the - // native library. The style content is never composed without a presentation, so it stops here too. + // Same guard as MeshMap, and in the same place: the style content is never composed without a presentation, so + // it stops here too. Like MeshMap's, it covers the view and not the state. if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) val zoomRange = basemaps.current.zoomRange() @@ -104,6 +112,8 @@ internal fun SecondaryMapSurface( CameraConstraints(minZoom = zoomRange.start.toDouble(), maxZoom = zoomRange.endInclusive.toDouble()), interactions = interactions, uiOptions = uiOptions, + // The library's default, spelled out so the collapsed branch stays a narrowing of it. + overlay = { include(if (showAttribution) MaplibreOverlay.Default else CollapsedAttributionOrnaments) }, ) } @@ -153,8 +163,9 @@ internal fun FitBoundsOnceVisible( // is cancelled by user input as well as by [key]: a fit lost that way is not retried until [key] changes // again, which for these maps may be never. val hasViewport = mapState.viewport != null + val fitPadding = padding.toDpPadding(LocalLayoutDirection.current) LaunchedEffect(key, hasViewport) { if (!hasViewport) return@LaunchedEffect - currentBounds()?.let { mapState.frameBounds(it, padding = padding) } + currentBounds()?.let { mapState.frameBounds(it, padding = fitPadding) } } } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt index 8c80fcffcd..11878bfb09 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt @@ -42,6 +42,7 @@ import kotlinx.serialization.json.put import org.jetbrains.compose.resources.stringResource import org.maplibre.compose.camera.CameraAnimation import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.CameraUpdate import org.maplibre.compose.expressions.dsl.asBoolean import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature @@ -117,10 +118,7 @@ fun MapLibreInlineMap( // first composition does not animate to where the camera already is. LaunchedEffect(target) { if (mapState.cameraPosition.target != target) { - mapState.animateCameraPosition( - mapState.cameraPosition.copy(target = target), - animation = CameraAnimation.Ease(), - ) + mapState.animateCamera(CameraUpdate(target = target), CameraAnimation.Ease()) } } @@ -132,7 +130,12 @@ fun MapLibreInlineMap( // The same control every other map uses, at the same size. A shrunken variant was tried and looked out of place // against the rest of the map chrome for the ~25% of height it saved. Box(modifier = modifier) { - SecondaryMapSurface(mapState = mapState, basemaps = basemaps, uiOptions = InlineMapUiOptions) + SecondaryMapSurface( + mapState = mapState, + basemaps = basemaps, + uiOptions = InlineMapUiOptions, + showAttribution = false, + ) MapZoom(mapState = mapState, basemap = basemaps.current) } @@ -168,7 +171,7 @@ fun MapLibreTracerouteMap( FitBoundsOnceVisible(mapState = mapState, key = hops) { nodesBoundingBox(hops) } Box(modifier = modifier) { - SecondaryMapSurface(mapState = mapState, basemaps = basemaps) + SecondaryMapSurface(mapState = mapState, basemaps = basemaps, showAttribution = false) SecondaryMapChrome(mapState = mapState, basemaps = basemaps) } } @@ -228,7 +231,7 @@ fun MapLibreDiscoveryMap( // snippet, so the tapped node's numbers go at the foot of the map. DiscoveryNodeCard( node = selectedNode, - // Clear of the logo and attribution row, which the styles are licensed on condition of showing. + // Clear of the wordmark and attribution button along the bottom edge, which every map shows. modifier = Modifier.align(Alignment.BottomStart) .padding(start = CARD_INSET.dp, end = CARD_INSET.dp, bottom = ORNAMENT_CLEARANCE.dp), @@ -444,7 +447,7 @@ internal val SecondaryMapFitPadding = PaddingValues(start = 48.dp, top = 64.dp, internal const val CARD_INSET = 8 /** - * Height to leave for the map's logo and attribution row, which the styles are licensed on condition of showing. + * Height to leave for the map's wordmark and attribution button, which every map shows. * * See [org.meshtastic.feature.map.maplibre.component.MeshMapOrnaments]. */ diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/TileEstimate.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/TileEstimate.kt index 0363dd3cae..a57d8e19ef 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/TileEstimate.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/TileEstimate.kt @@ -17,11 +17,8 @@ package org.meshtastic.feature.map.maplibre import org.maplibre.spatialk.geojson.BoundingBox -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.floor -import kotlin.math.ln -import kotlin.math.tan +import org.meshtastic.feature.map.maplibre.terrain.toGeoBounds +import org.meshtastic.feature.map.terrain.TerrainTileMath /** * How many tiles cover [this] region between [minZoom] and [maxZoom] inclusive. @@ -33,33 +30,6 @@ import kotlin.math.tan * Standard slippy-map arithmetic, so it matches what the renderer will actually request. */ internal fun BoundingBox.tileCount(minZoom: Int, maxZoom: Int): Long { - if (maxZoom < minZoom) return 0L - - return (minZoom..maxZoom).sumOf { zoom -> - val span = 1 shl zoom - val left = longitudeToTileX(west, span) - val right = longitudeToTileX(east, span) - // Tile rows run north to south, so the northern edge gives the lower index. - val top = latitudeToTileY(north, span) - val bottom = latitudeToTileY(south, span) - - // A box straddling the antimeridian arrives with west > east: its columns wrap around the tile grid, - // and the direct difference would go negative. - val columns = if (right >= left) right - left + 1 else span - left + right + 1 - columns.toLong() * (bottom - top + 1).toLong() - } + val bounds = toGeoBounds() + return (minZoom..maxZoom).sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } } - -private fun longitudeToTileX(longitude: Double, span: Int): Int = - floor((longitude + HALF_TURN) / FULL_TURN * span).toInt().coerceIn(0, span - 1) - -private fun latitudeToTileY(latitude: Double, span: Int): Int { - // Clamped to the Mercator limit: the projection runs to infinity at the poles. - val radians = latitude.coerceIn(-MERCATOR_LIMIT, MERCATOR_LIMIT) * PI / HALF_TURN - val projected = ln(tan(radians) + 1.0 / cos(radians)) / PI - return floor((1.0 - projected) / 2.0 * span).toInt().coerceIn(0, span - 1) -} - -private const val HALF_TURN = 180.0 -private const val FULL_TURN = 360.0 -private const val MERCATOR_LIMIT = 85.05112878 diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt index 2535322733..019441f578 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt @@ -19,16 +19,16 @@ package org.meshtastic.feature.map.maplibre.component import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import org.jetbrains.compose.resources.stringResource import org.maplibre.compose.offline.DownloadProgress import org.maplibre.compose.offline.OfflinePack +import org.meshtastic.core.common.util.formatByteSize import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.map_cache_megabytes import org.meshtastic.core.resources.map_cache_size import org.meshtastic.core.resources.map_cache_tiles @@ -58,16 +58,16 @@ internal fun rememberCacheUsage(packs: Set): CacheUsage { } } } - .collectAsState(CacheUsage()) + .collectAsStateWithLifecycle(CacheUsage()) return usage } /** * How much disk the downloaded packs occupy. * - * The OSMdroid map reported this in MB, which is the number that answers "is this filling my phone". No capacity beside - * it: OSMdroid had one bounded SQLite cache, whereas MapLibre has explicitly downloaded packs the user deletes by hand - * plus a separate ambient cache, and quoting a ceiling that governs neither would be a lie. + * In the phone's own decimal units, since this is the number that answers "is this filling my phone". No capacity + * beside it: OSMdroid had one bounded SQLite cache, whereas MapLibre has explicitly downloaded packs the user deletes + * by hand plus a separate ambient cache, and quoting a ceiling that governs neither would be a lie. */ @Composable internal fun CacheUsageLine(storedBytes: Long, storedTiles: Long) { @@ -75,7 +75,7 @@ internal fun CacheUsageLine(storedBytes: Long, storedTiles: Long) { text = stringResource(Res.string.map_cache_size) + ": " + - stringResource(Res.string.map_cache_megabytes, storedBytes.megabytes()) + + formatByteSize(storedBytes) + " · " + stringResource(Res.string.map_cache_tiles, storedTiles.toInt()), style = MaterialTheme.typography.bodySmall, diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ImportedLayersSlot.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ImportedLayersSlot.kt index b897384111..537600adec 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ImportedLayersSlot.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ImportedLayersSlot.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.map.maplibre.component import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.koin.compose.koinInject import org.meshtastic.feature.map.component.CustomMapLayersSheet import org.meshtastic.feature.map.layers.LayerOpacityStore @@ -35,8 +35,8 @@ import org.meshtastic.feature.map.layers.rememberMapLayerPicker fun ImportedLayersSlot() { val manager: MapLayersManager = koinInject() val opacityStore: LayerOpacityStore = koinInject() - val layers by manager.mapLayers.collectAsState() - val opacity by opacityStore.opacity.collectAsState() + val layers by manager.mapLayers.collectAsStateWithLifecycle() + val opacity by opacityStore.opacity.collectAsStateWithLifecycle() val picker = rememberMapLayerPicker(onPick = manager::addMapLayer) CustomMapLayersSheet( diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapLayersButton.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapLayersButton.kt index b9e7a01878..3e5d8a848f 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapLayersButton.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapLayersButton.kt @@ -25,13 +25,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import org.meshtastic.core.resources.Res @@ -68,7 +68,7 @@ internal fun MapLayersButton( ) { var sheetVisible by remember { mutableStateOf(false) } val opacityStore: LayerOpacityStore = koinInject() - val opacity by opacityStore.opacity.collectAsState() + val opacity by opacityStore.opacity.collectAsStateWithLifecycle() MapButton( icon = MeshtasticIcons.Layers, diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt index b1b2462830..a5fdd27c81 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt @@ -16,51 +16,77 @@ */ package org.meshtastic.feature.map.maplibre.component +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.filter +import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.overlay.AttributionDefaults +import org.maplibre.compose.overlay.AttributionLinks import org.maplibre.compose.overlay.DisappearingScaleBar -import org.maplibre.compose.overlay.LocalCameraPadding +import org.maplibre.compose.overlay.LocalViewportInsets +import org.maplibre.compose.overlay.MaplibreLogo +import org.maplibre.compose.overlay.attributions import org.maplibre.compose.overlay.include import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay /** * The map's own ornaments: a scale bar while zooming, and the logo and attribution along the bottom. * - * This is `MapOverlay.Default` with its compass removed. The mesh map already has a compass in its toolbar — one that - * also toggles heading-lock — and drawing the library's as well would put two compasses on screen. The Google flavor + * This reproduces `MapOverlay.Default`, which is a scale bar plus `AttributionOnly`, and is spelled out rather than + * `include`d because the library's inset helper is internal to it. + * + * The library's own compass and zoom pair are absent by choice rather than by removal: the mesh map already has a + * compass in its toolbar — one that also toggles heading-lock — and draws its zoom pair as `MapZoom`. The Google flavor * makes the same call from the other direction with `compassEnabled = false`. * * The logo and attribution are deliberately kept: the styles this map serves are licensed on the condition that they - * are shown. Do not replace this with `MapOverlay.None`. + * are shown. Do not replace this with `MapOverlay.None`; the main map is where the credit is read, not hidden behind a + * tap. * * Scale-bar units are left to the library, which picks them by region — the same locale-driven approach the rest of the * app takes through `localeUnitsProvider`. */ internal val MeshMapOrnaments: MaplibreOverlay = MaplibreOverlay { val mapState = checkNotNull(LocalMapState.current) - val cameraPadding = LocalCameraPadding.current + val viewportInsets = LocalViewportInsets.current // A custom overlay fills the map and the library keeps its own inset helper internal, so the scale bar has to - // carry the camera padding, safe-area insets and edge margin that the built-in controls apply for themselves. + // carry the viewport insets, safe-area insets and edge margin that the built-in controls apply for themselves. Box( modifier = Modifier.fillMaxSize() - .padding(cameraPadding) - .consumeWindowInsets(cameraPadding) + .padding(viewportInsets) + .consumeWindowInsets(viewportInsets) .windowInsetsPadding(WindowInsets.safeDrawing) .padding(MaplibreOverlay.Spacing), ) { DisappearingScaleBar( - metersPerDp = mapState.viewport?.metersPerDpAtTarget ?: 0.0, - zoom = mapState.cameraPosition.zoom, + metersPerDp = { mapState.viewport?.metersPerDpAtTarget ?: 0.0 }, + zoom = { mapState.cameraPosition.zoom }, modifier = Modifier.align(Alignment.TopStart), ) } @@ -68,3 +94,86 @@ internal val MeshMapOrnaments: MaplibreOverlay = MaplibreOverlay { // The logo and the attribution button, in the places `MapOverlay.Default` puts them. include(MaplibreOverlay.AttributionOnly) } + +/** + * `MapOverlay.Default` with the credit collapsed, for a map too small to host the credit without it overflowing: the + * scale bar, the wordmark and the button all stay, and the button is what reveals the text. + * + * Composed rather than reached for because the library cannot be asked for it — [CollapsedAttributionButton] says why — + * and the insets are spelled out because its `DefaultControls` is internal to it. + */ +internal val CollapsedAttributionOrnaments: MaplibreOverlay = MaplibreOverlay { + val mapState = checkNotNull(LocalMapState.current) + val viewportInsets = LocalViewportInsets.current + Box( + modifier = + Modifier.fillMaxSize() + .padding(viewportInsets) + .consumeWindowInsets(viewportInsets) + .windowInsetsPadding(WindowInsets.safeDrawing) + .padding(MaplibreOverlay.Spacing), + ) { + DisappearingScaleBar( + metersPerDp = { mapState.viewport?.metersPerDpAtTarget ?: 0.0 }, + zoom = { mapState.cameraPosition.zoom }, + modifier = Modifier.align(Alignment.TopStart), + ) + MaplibreLogo(Modifier.align(Alignment.BottomStart)) + CollapsedAttributionButton(Modifier.align(Alignment.BottomEnd)) + } +} + +/** + * How much of the map's width the expanded credit may take, the rest going to the toggle. Under half, so the strip + * still reads as a strip on a thumbnail rather than a banner, and the map beside it stays usable. + */ +private const val CREDIT_WIDTH_FRACTION = 0.55f + +/** + * The library's attribution button, starting collapsed. + * + * `ExpandingAttributionButton` holds its own `expanded` flag, initialised to `true` and not settable from outside, so + * `collapsedStyle` only styles the branch it is already in and the credit still opens. Holding the flag here is what + * makes it start shut; everything else — icon, label, links — still comes from the library. + */ +@Composable +private fun CollapsedAttributionButton(modifier: Modifier = Modifier) { + val mapState = checkNotNull(LocalMapState.current) + val mapStyle = mapState.style + // Derived, not remembered: a basemap arrives as a style URL, so its sources are still empty on the first + // composition and a remembered list would never re-read. As in the library's own button. + val attributions by remember(mapStyle) { derivedStateOf { mapStyle.attributions() } } + if (attributions.isEmpty()) return + + var expanded by remember(mapStyle) { mutableStateOf(false) } + + // As the library's button: a gesture closes the credit, so it does not sit over a short map after a pan. Narrowed + // to gestures, or the programmatic moves our own zoom controls make would fold it away mid-animation. + LaunchedEffect(mapState) { + snapshotFlow { mapState.isCameraMoving && mapState.cameraMoveReason == CameraMoveReason.GESTURE } + .filter { it } + .collect { expanded = false } + } + + BoxWithConstraints(modifier.clip(RoundedCornerShape(24.dp)).background(AttributionDefaults.ContainerColor)) { + // Read out here, not inside AnimatedVisibility: that lambda's receiver hides this scope. + val creditMaxWidth = maxWidth * CREDIT_WIDTH_FRACTION + Row(verticalAlignment = Alignment.CenterVertically) { + // The toggle is last: the container is trailing-aligned, so a leading toggle would sit inboard of the + // credit + // and drift inwards as it opens. A Row mirrors itself, so last is the trailing edge in RTL too. + AnimatedVisibility(visible = expanded) { + Box( + Modifier + // `AttributionLinks` scrolls its own single line, so it only needs a ceiling: unbounded it + // measures at the credit's full width and sprawls past a short map. + .widthIn(max = creditMaxWidth) + .padding(start = 12.dp, end = 16.dp, top = 8.dp, bottom = 8.dp), + ) { + AttributionLinks(attributions = attributions, textStyle = AttributionDefaults.ContentTextStyle) + } + } + AttributionDefaults.button { expanded = !expanded } + } + } +} diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt index b0524a5328..5798688da8 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -37,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -44,16 +44,16 @@ import org.maplibre.compose.map.DefaultMapRuntime import org.maplibre.compose.offline.DownloadProgress import org.maplibre.compose.offline.DownloadStatus import org.maplibre.compose.offline.OfflineManager +import org.maplibre.compose.offline.OfflineManagerState import org.maplibre.compose.offline.OfflinePack import org.maplibre.compose.offline.OfflinePackDefinition import org.maplibre.spatialk.geojson.BoundingBox -import org.meshtastic.core.common.util.NumberFormatter +import org.meshtastic.core.common.util.formatByteSize import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.delete import org.meshtastic.core.resources.map_cache_manager -import org.meshtastic.core.resources.map_cache_megabytes import org.meshtastic.core.resources.map_cache_tiles import org.meshtastic.core.resources.map_download_status_complete import org.meshtastic.core.resources.map_download_status_downloading @@ -91,7 +91,8 @@ internal fun OfflineMapsSection(target: OfflineMapTarget, onShowRegion: (Boundin // the one every map here uses, so its packs are the ones the user sees on the map. val manager = DefaultMapRuntime.instance.offlineManager val scope = rememberCoroutineScope() - val packs by manager.packs.collectAsState() + val managerState by manager.state.collectAsStateWithLifecycle() + val packs = (managerState as? OfflineManagerState.Ready)?.packs.orEmpty() // A pack definition now carries the pixel ratio it was downloaded at, so the tiles match this display. val pixelRatio = LocalDensity.current.density @@ -165,7 +166,7 @@ private fun OfflinePackRow( onToggle: () -> Unit, onDelete: () -> Unit, ) { - val progress = pack.downloadProgress.collectAsState().value + val progress = pack.downloadProgress.collectAsStateWithLifecycle().value val bounds = (pack.definition as? OfflinePackDefinition.TilePyramid)?.bounds Row( @@ -267,7 +268,7 @@ private fun DownloadProgress.fraction(): Float = when (this) { * One line describing a pack's state, assembled from resources rather than written in English. * * `status.name` went straight into the UI before, so every locale read the library's own enum constants. The tile count - * and byte size reuse the strings the cache figures above already use, which keeps one set of units to translate. + * and byte size render as the cache figures above render them. */ @Composable private fun DownloadProgress.summary(): String = when (this) { @@ -281,7 +282,7 @@ private fun DownloadProgress.summary(): String = when (this) { }, ), stringResource(Res.string.map_cache_tiles, completedTileCount.toInt()), - stringResource(Res.string.map_cache_megabytes, completedResourceBytes.megabytes()), + formatByteSize(completedResourceBytes), ) .joinToString(SUMMARY_SEPARATOR) @@ -293,21 +294,12 @@ private fun DownloadProgress.summary(): String = when (this) { DownloadProgress.Unknown -> EM_DASH } -/** - * Bytes as megabytes, to one decimal place. - * - * Decimal megabytes rather than mebibytes: this number sits next to a phone's own storage figures, and those are - * decimal. - */ -internal fun Long.megabytes(): String = NumberFormatter.format(this.toDouble() / BYTES_PER_MEGABYTE, 1) - private fun Double.round(): String { // Rounded, not truncated, so a negative coordinate labels the same way as its positive twin. val scaled = (this * COORD_SCALE).roundToInt() / COORD_SCALE return scaled.toString() } -private const val BYTES_PER_MEGABYTE = 1_000_000.0 private const val PACK_ROW_TEXT_FRACTION = 0.8f private const val PACK_EXTRA_ZOOM_LEVELS = 2 diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineTerrainSection.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineTerrainSection.kt index 8694e69f12..c00e1d2abe 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineTerrainSection.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineTerrainSection.kt @@ -30,19 +30,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.maplibre.spatialk.geojson.BoundingBox +import org.meshtastic.core.common.util.formatByteSize import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.delete -import org.meshtastic.core.resources.map_cache_megabytes import org.meshtastic.core.resources.map_cache_size import org.meshtastic.core.resources.map_cache_tiles import org.meshtastic.core.resources.map_select_download_region @@ -59,13 +59,13 @@ import org.meshtastic.core.ui.icon.Delete import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.map.maplibre.terrain.OfflineTerrainRegion import org.meshtastic.feature.map.maplibre.terrain.OfflineTerrainRepository -import org.meshtastic.feature.map.maplibre.terrain.estimateTerrainTiles import org.meshtastic.feature.map.maplibre.terrain.toBoundingBox import org.meshtastic.feature.map.maplibre.terrain.toGeoBounds import org.meshtastic.feature.map.terrain.GeoBounds import org.meshtastic.feature.map.terrain.MapterhornEndpoints import org.meshtastic.feature.map.terrain.TerrainDownloadState import org.meshtastic.feature.map.terrain.TerrainRegionExtractor +import org.meshtastic.feature.map.terrain.terrainTileCount /** * Offline terrain — hillshade and elevation contours for the viewport, downloaded as its own section of the layers @@ -80,17 +80,17 @@ import org.meshtastic.feature.map.terrain.TerrainRegionExtractor internal fun OfflineTerrainSection(target: OfflineMapTarget, onShowRegion: (BoundingBox) -> Unit) { val repository = remember { OfflineTerrainRepository.default } val scope = rememberCoroutineScope() - val region by repository.region.collectAsState() + val region by repository.region.collectAsStateWithLifecycle() // Not a local composable state: startDownload runs on the repository's own scope (see its doc comment for // why), so the state it reports has to be read from there too, or progress would appear to vanish the moment // this composable leaves composition and reappear wrong on the next one. - val downloadState by repository.downloadState.collectAsState() + val downloadState by repository.downloadState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { repository.refresh() } + LaunchedEffect(repository) { repository.refresh() } val bounds = target.bounds() val maxZoom = target.terrainMaxZoom() - val estimate = bounds?.let { estimateTerrainTiles(it.toGeoBounds(), maxZoom) } ?: 0L + val estimate = bounds?.let { terrainTileCount(it.toGeoBounds(), maxZoom) } ?: 0L val overLimit = estimate > TerrainRegionExtractor.MAX_TILES val isDownloading = downloadState is TerrainDownloadState.InProgress @@ -201,7 +201,7 @@ private fun DownloadedTerrainRow(region: OfflineTerrainRegion, onShow: () -> Uni stringResource( Res.string.offline_terrain_cache_detail, stringResource(Res.string.map_cache_size), - stringResource(Res.string.map_cache_megabytes, region.byteSize.megabytes()), + formatByteSize(region.byteSize), stringResource(Res.string.map_cache_tiles, region.tileCount.toInt()), ), style = MaterialTheme.typography.bodyMedium, @@ -224,8 +224,8 @@ private fun DownloadedTerrainRow(region: OfflineTerrainRegion, onShow: () -> Uni * The zoom levels a terrain download covers: the current level plus a couple deeper, mirroring [OfflineMapTarget]'s own * private `zoomRange` convention for the base map's offline packs. * - * Bounded by [MapterhornEndpoints.REGIONAL_MAX_ZOOM] rather than MapLibre's own 0..20, since [estimateTerrainTiles] and - * [TerrainRegionExtractor] never fetch anything deeper than that regardless of what is asked for. + * Bounded by [MapterhornEndpoints.REGIONAL_MAX_ZOOM] rather than MapLibre's own 0..20, since [terrainTileCount] and + * [TerrainRegionExtractor] never count or fetch anything deeper than that regardless of what is asked for. */ private fun OfflineMapTarget.terrainMaxZoom(): Int { val current = zoom().toInt().coerceIn(0, MapterhornEndpoints.REGIONAL_MAX_ZOOM) diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt index c92ec965a5..e5b1dbf5f2 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import kotlinx.coroutines.launch import org.maplibre.compose.camera.CameraAnimation +import org.maplibre.compose.camera.CameraUpdate import org.maplibre.compose.map.MapState import org.meshtastic.feature.map.component.MapControlsOverlay @@ -54,12 +55,7 @@ internal fun SecondaryMapControls( filterDropdownContent = { filterMenu?.invoke(filterMenuExpanded) { filterMenuExpanded = false } }, bearing = mapState.cameraPosition.bearing.toFloat(), onCompassClick = { - scope.launch { - mapState.animateCameraPosition( - mapState.cameraPosition.copy(bearing = 0.0), - animation = CameraAnimation.Ease(), - ) - } + scope.launch { mapState.animateCamera(CameraUpdate(bearing = 0.0), CameraAnimation.Ease()) } }, mapTypeContent = { BasemapButton(selection = basemaps) }, onToggleLocationTracking = null, diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt index 6e86c9f088..e14a604791 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt @@ -37,6 +37,9 @@ private const val ZOOM_INSET = 8 /** * Lift above the attribution button, which is 40dp of icon plus its pill padding and shares this corner. More than the * cards need on the other side, where only the 23dp wordmark sits. + * + * The button is there either way — the small maps only collapse its credit, see [CollapsedAttributionOrnaments] — so + * this clears it whether it is expanded or not. */ private const val ZOOM_BOTTOM_INSET = 56 @@ -47,8 +50,8 @@ private const val ZOOM_BOTTOM_INSET = 56 * extension because the placement is the point: a caller that had to align it itself would eventually align it * somewhere else. * - * Lifted clear of the logo and attribution row along the bottom edge, which the styles are licensed on condition of - * showing — see [MeshMapOrnaments]. + * Lifted clear of the wordmark and attribution button along the bottom edge, which every map shows. See + * [MeshMapOrnaments] for why the main map carries the credit outright and the small maps behind the button. */ @Composable internal fun BoxScope.MapZoom(mapState: MapState, basemap: Basemap) { diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeatures.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeatures.kt index d5f1382d93..a0481cdff1 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeatures.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeatures.kt @@ -65,7 +65,7 @@ fun waypointsToFeatureCollection(waypoints: Collection): FeatureColl put(WaypointFeatureKeys.WAYPOINT_ID, waypoint.id) put(WaypointFeatureKeys.NAME, waypoint.name) put(WaypointFeatureKeys.ICON, iconGlyph(waypoint.icon)) - put(WaypointFeatureKeys.IS_LOCKED, (waypoint.locked_to ?: 0) != 0) + put(WaypointFeatureKeys.IS_LOCKED, waypoint.locked_to != 0) }, ) }, diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/CustomLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/CustomLayers.kt index 1e3e8dbb57..ddcb70f4de 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/CustomLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/CustomLayers.kt @@ -17,16 +17,15 @@ package org.meshtastic.feature.map.maplibre.layers import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import coil3.compose.AsyncImagePainter import coil3.compose.LocalPlatformContext @@ -59,6 +58,7 @@ import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberImageSource import org.maplibre.compose.util.PositionQuad +import org.maplibre.compose.util.PreparedImage import org.maplibre.spatialk.geojson.Position import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.safeCatching @@ -86,6 +86,7 @@ internal fun CustomLayers(layers: List, opacity: Map } /** One imported overlay: a source, and the three layers that between them can draw anything in it. */ +@Suppress("SpreadOperator") // switch() only takes its cases as varargs @Composable private fun ImportedLayer(layer: CustomLayer, opacity: Float) { val source = rememberGeoJsonSource(data = GeoJsonData.Uri(layer.uri)) @@ -184,7 +185,7 @@ private fun rememberLayerIcons(urls: Set): Map { key(url) { val painter = rememberAsyncImagePainter(ImageRequest.Builder(LocalPlatformContext.current).data(url).build()) - val state by painter.state.collectAsState() + val state by painter.state.collectAsStateWithLifecycle() // The loaded painter, not the async wrapper around it: MapLibre rasterizes a painter outside the // composition driving it, where an AsyncImagePainter draws nothing. (state as? AsyncImagePainter.State.Success)?.let { loaded[url] = it.painter } @@ -205,13 +206,14 @@ private fun GroundOverlayLayer(layerId: String, index: Int, overlay: LayerGround // Decoded off the composition thread: an ESRI export's tile is routinely multi-megapixel, and a synchronous // decode in `remember` would hitch the map for every overlay on every first composition. val image by - produceState(initialValue = null, overlay.imagePath) { + produceState(initialValue = null, overlay.imagePath) { value = withContext(ioDispatcher) { safeCatching { mapLayerFileSystem() .read(overlay.imagePath.toLocalPath()) { readByteArray() } .decodeToImageBitmap() + .let(PreparedImage::fromBitmap) } .onFailure { Logger.withTag("CustomLayers").w(it) { "Could not decode a ground overlay image" } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt index 5c60df41a4..3be6e3f7b7 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt @@ -152,6 +152,7 @@ private fun chipSortKey(): Expression = switch( * * Shared by the node maps and the discovery map, which draws differently coloured chips for the same reason. */ +@Suppress("SpreadOperator") // switch() only takes its cases as varargs @Composable internal fun MapChipLayer( id: String, @@ -277,7 +278,8 @@ private class ChipPainter( } else if (layout != null) { drawText( textLayoutResult = layout, - topLeft = Offset( + topLeft = + Offset( x = (size.width - layout.size.width) / 2f, y = (size.height - layout.size.height) / 2f, ), diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/RenderableLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/RenderableLayers.kt index a515847147..f6123e7d4f 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/RenderableLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/RenderableLayers.kt @@ -75,24 +75,23 @@ fun rememberRenderableLayers(manager: MapLayersManager, layers: List - when { - layer.layerType != LayerType.KML -> - layer.uri?.let { CustomLayer(id = layer.id, uri = it, refreshToken = layer.refreshToken) } + val renderable = layers.mapNotNull { layer -> + when { + layer.layerType != LayerType.KML -> + layer.uri?.let { CustomLayer(id = layer.id, uri = it, refreshToken = layer.refreshToken) } - else -> - converted[layer.conversionKey()]?.let { conversion -> - CustomLayer( - id = layer.id, - uri = conversion.geoJsonUri, - refreshToken = layer.refreshToken, - icons = conversion.icons, - groundOverlays = conversion.groundOverlays, - ) - } - } + else -> + converted[layer.conversionKey()]?.let { conversion -> + CustomLayer( + id = layer.id, + uri = conversion.geoJsonUri, + refreshToken = layer.refreshToken, + icons = conversion.icons, + groundOverlays = conversion.groundOverlays, + ) + } } + } // The renderer has to know a layer's icons before it composes, and the only place they exist is the GeoJSON // itself. Reading the finished document rather than threading the set out of the KML converter means an imported @@ -133,8 +132,6 @@ private suspend fun scanLayerIcons(uri: String): Set = withContext(ioDis private fun MapLayerItem.conversionKey(): String = "$id@$refreshToken" -private fun CustomLayer.conversionKey(): String = "$id@$refreshToken" - /** What one KML conversion produced: the GeoJSON file's URI and the draped images. */ internal data class ConvertedKml( val geoJsonUri: String, @@ -167,7 +164,7 @@ private suspend fun convertKmlLayer(manager: MapLayersManager, layer: MapLayerIt val geoJson = fs.read(target) { readUtf8() } return@safeCatching ConvertedKml( geoJsonUri = "$FILE_URI_PREFIX$target", - groundOverlays = cachedOverlays.orEmpty(), + groundOverlays = cachedOverlays, icons = trustedGeoJsonIconUrls(geoJson), ) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt index 14b2acd0a7..8aa870b093 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt @@ -18,11 +18,11 @@ package org.meshtastic.feature.map.maplibre.layers import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonObject import org.maplibre.compose.expressions.dsl.asNumber @@ -74,9 +74,9 @@ import org.meshtastic.feature.map.terrain.TerrainTileMath @Composable internal fun TerrainLayers(viewportBounds: BoundingBox?, zoom: Double, displayUnits: MeasurementSystem) { val repository = remember { OfflineTerrainRepository.default } - val region by repository.region.collectAsState() + val region by repository.region.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { repository.refresh() } + LaunchedEffect(repository) { repository.refresh() } // Two early returns (detekt's ReturnCount limit), kept inline rather than in a helper function: a helper // returning a resolved nullable can't hand the compiler back a smart-cast on *this* function's own diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt index cdd2bcffba..4a1dc97c5e 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt @@ -30,8 +30,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okio.FileSystem import okio.IOException @@ -151,17 +149,16 @@ class OfflineTerrainRepository(private val fileSystem: FileSystem, private val b fun startDownload(bounds: GeoBounds, maxZoom: Int) { if (downloadJob?.isActive == true) return _downloadState.value = null - downloadJob = - scope.launch { - try { - download(bounds, maxZoom).collect { state -> _downloadState.value = state } - } catch (e: IOException) { - // download() reports the extractor's and manifest's own failures as states; this is the - // pre-extraction disk work (clearing the previous region), which otherwise leaves the state stuck. - Logger.withTag(LOG_TAG).w(e) { "Offline terrain download failed before extraction" } - _downloadState.value = TerrainDownloadState.Failed(TerrainDownloadFailure.IO_ERROR) - } + downloadJob = scope.launch { + try { + download(bounds, maxZoom).collect { state -> _downloadState.value = state } + } catch (e: IOException) { + // download() reports the extractor's and manifest's own failures as states; this is the + // pre-extraction disk work (clearing the previous region), which otherwise leaves the state stuck. + Logger.withTag(LOG_TAG).w(e) { "Offline terrain download failed before extraction" } + _downloadState.value = TerrainDownloadState.Failed(TerrainDownloadFailure.IO_ERROR) } + } } /** Deletes the current region's tiles and manifest, leaving nothing downloaded. A no-op if there is none. */ diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimate.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimate.kt deleted file mode 100644 index c67fc1371f..0000000000 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimate.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.map.maplibre.terrain - -import org.meshtastic.feature.map.terrain.GeoBounds -import org.meshtastic.feature.map.terrain.MapterhornEndpoints -import org.meshtastic.feature.map.terrain.TerrainTileMath - -/** - * How many tiles [TerrainRegionExtractor][org.meshtastic.feature.map.terrain.TerrainRegionExtractor] would fetch for - * [bounds] up to [maxZoom] — the global tier always, the regional tier only where [MapterhornEndpoints.regionalUrlFor] - * finds one. - * - * The counting itself is [TerrainTileMath.tileCountAt] — the same O(1)-per-zoom corner arithmetic the extractor uses to - * bound memory before it enumerates tiles, so this and the extractor can never silently disagree on *how many* a zoom - * level covers. What's re-derived here, network-free, is only the *which zoom levels / which tier* decision from the - * extractor's own suspending `download` [kotlinx.coroutines.flow.Flow] — matching - * [org.meshtastic.feature.map.maplibre.tileCount]'s own "shown before a download starts" role for the base offline - * layer. If the extractor's own tier/zoom-range logic ever changes, this must change with it — that coupling is - * intentional, not an oversight; a unit test pins it against [MapterhornEndpoints]'s real constants rather than a copy - * of them. - */ -internal fun estimateTerrainTiles(bounds: GeoBounds, maxZoom: Int): Long { - val globalZoomRange = 0..minOf(maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM) - val globalCount = globalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - - val hasRegionalTier = - maxZoom > MapterhornEndpoints.GLOBAL_MAX_ZOOM && MapterhornEndpoints.regionalUrlFor(bounds) != null - val regionalCount = - if (hasRegionalTier) { - val regionalMaxZoom = minOf(maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM) - val regionalZoomRange = MapterhornEndpoints.REGIONAL_MIN_ZOOM..regionalMaxZoom - regionalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - } else { - 0L - } - - return globalCount + regionalCount -} diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/ContourFeaturesTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/ContourFeaturesTest.kt index e54464b1ce..42545a0bba 100644 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/ContourFeaturesTest.kt +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/ContourFeaturesTest.kt @@ -18,7 +18,6 @@ package org.meshtastic.feature.map.maplibre.geojson import kotlinx.serialization.json.float import kotlinx.serialization.json.jsonPrimitive -import org.maplibre.spatialk.geojson.LineString import org.meshtastic.feature.map.terrain.ContourLine import org.meshtastic.feature.map.terrain.ContourPoint import org.meshtastic.feature.map.terrain.TerrainTileMath @@ -38,7 +37,7 @@ class ContourFeaturesTest { val features = contourLinesToFeatures(tile, listOf(line), zoom = 12, metric = true) assertEquals(1, features.size) - assertEquals(2, (features.single().geometry as LineString).coordinates.size) + assertEquals(2, features.single().geometry.coordinates.size) } @Test @@ -51,7 +50,7 @@ class ContourFeaturesTest { fun `tile-local points convert to the same lat lon TerrainTileMath's own inverse would produce`() { val line = ContourLine(elevationMeters = 100f, points = listOf(ContourPoint(0f, 0f), ContourPoint(1f, 1f))) val feature = contourLinesToFeatures(tile, listOf(line), zoom = 12, metric = true).single() - val positions = (feature.geometry as LineString).coordinates + val positions = feature.geometry.coordinates val expectedFirst = TerrainTileMath.lonLatAt(tile, 0f, 0f) assertEquals(expectedFirst.longitude, positions.first().longitude) diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimateTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimateTest.kt deleted file mode 100644 index faeefeacf4..0000000000 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/terrain/TerrainDownloadEstimateTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.map.maplibre.terrain - -import org.meshtastic.feature.map.terrain.GeoBounds -import org.meshtastic.feature.map.terrain.MapterhornEndpoints -import org.meshtastic.feature.map.terrain.TerrainTileMath -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class TerrainDownloadEstimateTest { - - private val tinyBounds = GeoBounds(south = 47.6, west = -122.4, north = 47.61, east = -122.39) - - @Test - fun `matches a manual sum of tilesAt across the global zoom range only`() { - val maxZoom = MapterhornEndpoints.GLOBAL_MAX_ZOOM - val expected = (0..maxZoom).sumOf { zoom -> TerrainTileMath.tilesAt(zoom, tinyBounds).size.toLong() } - - assertEquals(expected, estimateTerrainTiles(tinyBounds, maxZoom)) - } - - @Test - fun `a maxZoom at or below the global ceiling never counts a regional tier`() { - val globalOnly = estimateTerrainTiles(tinyBounds, MapterhornEndpoints.GLOBAL_MAX_ZOOM) - val deeper = estimateTerrainTiles(tinyBounds, MapterhornEndpoints.GLOBAL_MAX_ZOOM + 1) - - // The regional tier only exists past the global ceiling — one more zoom level must add regional tiles too. - assertTrue(deeper > globalOnly) - } - - @Test - fun `a huge box that never fits a single regional archive gets no regional tier`() { - // Spans more than one z6 tile, so MapterhornEndpoints.regionalUrlFor returns null. - val huge = GeoBounds(south = -60.0, west = -170.0, north = 60.0, east = 170.0) - - val atGlobalCeiling = estimateTerrainTiles(huge, MapterhornEndpoints.GLOBAL_MAX_ZOOM) - val past = estimateTerrainTiles(huge, MapterhornEndpoints.GLOBAL_MAX_ZOOM + 1) - - // Past the global ceiling with no regional archive, nothing further is fetched — the count doesn't grow. - assertEquals(atGlobalCeiling, past) - } - - @Test - fun `negative maxZoom counts nothing`() { - assertEquals(0L, estimateTerrainTiles(tinyBounds, -1)) - } - - @Test - fun `maxZoom 0 counts the single world-covering z0 tile`() { - // Unlike a negative maxZoom, 0 is a real, valid zoom range (0..0) -- it legitimately counts the one z0 tile - // that covers the whole world, not nothing. - assertEquals(1L, estimateTerrainTiles(tinyBounds, 0)) - } -} diff --git a/feature/map-terrain/build.gradle.kts b/feature/map-terrain/build.gradle.kts index b5afb7c40e..2461dcba0f 100644 --- a/feature/map-terrain/build.gradle.kts +++ b/feature/map-terrain/build.gradle.kts @@ -14,7 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -plugins { alias(libs.plugins.meshtastic.kmp.feature) } +plugins { + alias(libs.plugins.meshtastic.kmp.feature) + alias(libs.plugins.meshtastic.kmp.jvm.android) +} // Offline terrain: Terrarium elevation decode, hillshade shading, and contour-line generation — // pure computation shared by both the Google flavor (androidApp/src/google, pre-rendered hillshade @@ -36,7 +39,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(projects.core.common) implementation(libs.kotlinx.coroutines.core) // Local terrain-tile storage is a plain file hierarchy, not SQLite: unlike the base offline layer's // Google-only archive (which can assume Android's SQLite), this module's storage must also work on @@ -46,13 +48,10 @@ kotlin { commonTest.dependencies { implementation(libs.okio.fakefilesystem) } - // ch.poole.geo.pmtiles:Reader is a plain Java library, usable identically from both the android and - // jvm targets — but KMP has no built-in "android+jvm, not native" source set to put it in once, so the - // small amount of code wrapping it is duplicated between androidMain and jvmMain, same as - // ElevationTile's platform-specific decode actuals. - androidMain.dependencies { implementation(libs.pmtiles.reader) } + // ch.poole.geo.pmtiles:Reader is a plain Java library, so the tile fetcher wrapping it is shared by the + // android and jvm targets. ElevationTile's decode still differs per platform (BitmapFactory vs Skia). + getByName("jvmAndroidMain") { dependencies { implementation(libs.pmtiles.reader) } } jvmMain.dependencies { - implementation(libs.pmtiles.reader) // Skia's Image decoder reaches WebP directly; brought in transitively by Compose // Multiplatform's desktop UI artifact, which the `meshtastic.kmp.feature` convention plugin // already applies — see feature/map-maplibre's identical jvmTest dependency for precedent. diff --git a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/ContourGenerator.kt b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/ContourGenerator.kt index 140b908129..c05e198cba 100644 --- a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/ContourGenerator.kt +++ b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/ContourGenerator.kt @@ -118,10 +118,10 @@ object ContourGenerator { // this differs from the ordinary case, which only ever has one. ALL_EDGES_CROSSING -> { val centerAverage = (tl + tr + br + bl) / CORNER_COUNT - if (centerAverage >= level) { - listOf(Segment(topPoint!!, leftPoint!!), Segment(bottomPoint!!, rightPoint!!)) - } else { - listOf(Segment(topPoint!!, rightPoint!!), Segment(leftPoint!!, bottomPoint!!)) + when { + topPoint == null || rightPoint == null || bottomPoint == null || leftPoint == null -> emptyList() + centerAverage >= level -> listOf(Segment(topPoint, leftPoint), Segment(bottomPoint, rightPoint)) + else -> listOf(Segment(topPoint, rightPoint), Segment(leftPoint, bottomPoint)) } } diff --git a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainRegionExtractor.kt b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainRegionExtractor.kt index 1ae90f7566..9d847c3c51 100644 --- a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainRegionExtractor.kt +++ b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainRegionExtractor.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.feature.map.terrain +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -55,25 +56,16 @@ class TerrainRegionExtractor( fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? } + @Suppress("SuspendFunSwallowedCancellation") // cancellation deletes the partial region, then is rethrown fun download(bounds: GeoBounds, maxZoom: Int): Flow = flow { - val globalZoomRange = 0..minOf(maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM) - val regionalUrl = - if (maxZoom > MapterhornEndpoints.GLOBAL_MAX_ZOOM) MapterhornEndpoints.regionalUrlFor(bounds) else null - val regionalZoomRange = - MapterhornEndpoints.REGIONAL_MIN_ZOOM..minOf(maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM) + val globalZoomRange = globalTerrainZooms(maxZoom) + val regionalZoomRange = regionalTerrainZooms(bounds, maxZoom) + val regionalUrl = regionalZoomRange?.let { MapterhornEndpoints.regionalUrlFor(bounds) } - // Counted cheaply — via TerrainTileMath.tileCountAt's corner arithmetic, O(1) per zoom — before anything is - // materialized. A single regional z6 tile at maxZoom 18 is ~16.7M tiles at z18 alone; flatMap-ing a TileIndex - // per tile before this check runs would allocate that whole list first and risk OOM on exactly the oversized + // Counted from tile corners before anything is materialized. A single regional z6 tile at maxZoom 18 is ~16.7M + // tiles at z18 alone; enumerating before this check would allocate that whole list on exactly the oversized // requests this limit exists to reject. - val globalCount = globalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - val regionalCount = - if (regionalUrl != null) { - regionalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) } - } else { - 0L - } - val totalTiles = globalCount + regionalCount + val totalTiles = terrainTileCount(bounds, maxZoom) if (totalTiles > MAX_TILES) { emit(TerrainDownloadState.Failed(TerrainDownloadFailure.TILE_LIMIT_EXCEEDED)) return@flow @@ -85,12 +77,7 @@ class TerrainRegionExtractor( // Only materialized now that the cheap count above has confirmed it's under MAX_TILES. val globalTiles = globalZoomRange.flatMap { zoom -> TerrainTileMath.tilesAt(zoom, bounds) } - val regionalTiles = - if (regionalUrl != null) { - regionalZoomRange.flatMap { zoom -> TerrainTileMath.tilesAt(zoom, bounds) } - } else { - emptyList() - } + val regionalTiles = regionalZoomRange?.flatMap { zoom -> TerrainTileMath.tilesAt(zoom, bounds) }.orEmpty() val total = totalTiles.toInt() var global = FetchResult(processed = 0, stored = 0) @@ -104,6 +91,10 @@ class TerrainRegionExtractor( regional = fetchInto(regionalUrl, TerrainSource.REGIONAL, regionalTiles, global.processed, total) { emit(it) } } + } catch (e: CancellationException) { + // A cancelled download leaves no partial region behind, and nothing may be emitted after cancellation. + store.deleteAll() + throw e } catch (_: Exception) { store.deleteAll() emit(TerrainDownloadState.Failed(TerrainDownloadFailure.IO_ERROR)) diff --git a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCount.kt b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCount.kt new file mode 100644 index 0000000000..82bd51aec8 --- /dev/null +++ b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCount.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.terrain + +/** + * How many tiles [TerrainRegionExtractor.download] fetches for [bounds] up to [maxZoom]: the global tier always, the + * regional tier only where [MapterhornEndpoints.regionalUrlFor] finds an archive. Counted from tile corners, O(1) per + * zoom, so it is safe to call before deciding whether a download fits. + */ +fun terrainTileCount(bounds: GeoBounds, maxZoom: Int): Long { + val regional = regionalTerrainZooms(bounds, maxZoom)?.let { tileCount(bounds, it) } ?: 0L + return tileCount(bounds, globalTerrainZooms(maxZoom)) + regional +} + +/** Zoom levels fetched from the global archive for a download to [maxZoom]. */ +internal fun globalTerrainZooms(maxZoom: Int): IntRange = 0..minOf(maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM) + +/** Zoom levels fetched from the regional archive, or null when [bounds] has none or [maxZoom] stays global. */ +internal fun regionalTerrainZooms(bounds: GeoBounds, maxZoom: Int): IntRange? = + if (maxZoom > MapterhornEndpoints.GLOBAL_MAX_ZOOM && MapterhornEndpoints.regionalUrlFor(bounds) != null) { + MapterhornEndpoints.REGIONAL_MIN_ZOOM..minOf(maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM) + } else { + null + } + +private fun tileCount(bounds: GeoBounds, zooms: IntRange): Long = zooms.sumOf { zoom -> + TerrainTileMath.tileCountAt(zoom, bounds) +} diff --git a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.kt b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.kt index fa439b5963..398166574d 100644 --- a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.kt +++ b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.kt @@ -18,16 +18,13 @@ package org.meshtastic.feature.map.terrain /** * Fetches individual tiles from a remote PMTiles archive by range request — never the whole archive, just the bytes of - * the tile asked for. Backed by `ch.poole.geo.pmtiles:Reader` (MIT-licensed), the same library the base offline layer - * uses for Protomaps; here it's pointed at Mapterhorn's Terrarium elevation archives instead. - * - * A plain Java library, so its wrapper is duplicated between `androidMain` and `jvmMain` rather than living once in - * `commonMain` — see this module's `build.gradle.kts` for why. + * the tile asked for. Here it's pointed at Mapterhorn's Terrarium elevation archives. */ -expect class TerrainTileFetcher(pmtilesUrl: String) : AutoCloseable { +interface TerrainTileFetcher : AutoCloseable { /** Raw tile bytes (WebP, Terrarium-encoded) at [zoom]/[x]/[y] — google/osm XYZ convention — or `null` if absent. */ fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? - - override fun close() } + +/** Opens the remote PMTiles archive at [pmtilesUrl]. */ +expect fun TerrainTileFetcher(pmtilesUrl: String): TerrainTileFetcher diff --git a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMath.kt b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMath.kt index cb1f07242e..3296464e25 100644 --- a/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMath.kt +++ b/feature/map-terrain/src/commonMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMath.kt @@ -33,22 +33,27 @@ data class TileIndex(val zoom: Int, val x: Int, val y: Int) data class LonLat(val longitude: Double, val latitude: Double) /** - * Standard XYZ/slippy-map Web Mercator tile math, self-contained here (rather than reused from either flavor's own copy - * — `feature/map-maplibre`'s `TileEstimate.kt` and the Google flavor's `WebMercatorTileMath` in the sibling - * `feat/map-google-pmtiles-offline` branch) so this module has no dependency in either direction on flavor-specific - * code — this module is a shared math library, not a consumer of one flavor's app code. + * Standard XYZ/slippy-map Web Mercator tile math. Depends on no map stack, so the terrain extractor, the Google + * flavor's offline maps and MapLibre's node clustering share it. */ object TerrainTileMath { private const val MAX_LATITUDE = 85.05112878 - fun tileAt(zoom: Int, latitude: Double, longitude: Double): TileIndex { - val n = 2.0.pow(zoom) + /** Where a point falls on the Web Mercator world, from 0.0 at the northwest corner to 1.0 on each axis. */ + fun worldFraction(latitude: Double, longitude: Double): Pair { val clampedLat = latitude.coerceIn(-MAX_LATITUDE, MAX_LATITUDE) val latRad = clampedLat * PI / HALF_TURN_DEGREES - val x = (((longitude + FULL_TURN_DEGREES / 2) / FULL_TURN_DEGREES) * n).toInt().coerceIn(0, (n - 1).toInt()) - val y = (((1.0 - asinh(tan(latRad)) / PI) / 2.0) * n).toInt().coerceIn(0, (n - 1).toInt()) - return TileIndex(zoom, x, y) + val x = (longitude + FULL_TURN_DEGREES / 2) / FULL_TURN_DEGREES + val y = (1.0 - asinh(tan(latRad)) / PI) / 2.0 + return x to y + } + + fun tileAt(zoom: Int, latitude: Double, longitude: Double): TileIndex { + val n = 2.0.pow(zoom) + val max = (n - 1).toInt() + val (x, y) = worldFraction(latitude, longitude) + return TileIndex(zoom, (x * n).toInt().coerceIn(0, max), (y * n).toInt().coerceIn(0, max)) } /** The largest valid tile-column/row index at [zoom] — `2^zoom - 1`, the same bound [tileAt] clamps into. */ @@ -111,10 +116,14 @@ object TerrainTileMath { * * Standard inverse spherical Web Mercator — the mirror of [tileAt]'s own `asinh(tan(...))` forward transform. */ - fun lonLatAt(tile: TileIndex, localX: Float, localY: Float): LonLat { - val n = 2.0.pow(tile.zoom) - val x = (tile.x + localX) / n - val y = (tile.y + localY) / n + fun lonLatAt(tile: TileIndex, localX: Float, localY: Float): LonLat = + lonLatAt(tile.zoom, (tile.x + localX).toDouble(), (tile.y + localY).toDouble()) + + /** [lonLatAt] for a point given in fractional tile units at [zoom], e.g. `x = 3.25` is a quarter into column 3. */ + fun lonLatAt(zoom: Int, tileX: Double, tileY: Double): LonLat { + val n = 2.0.pow(zoom) + val x = tileX / n + val y = tileY / n val longitude = x * FULL_TURN_DEGREES - FULL_TURN_DEGREES / 2 val latitudeRadians = atan(sinh(PI * (1.0 - 2.0 * y))) val latitude = latitudeRadians * HALF_TURN_DEGREES / PI diff --git a/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCountTest.kt b/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCountTest.kt new file mode 100644 index 0000000000..58592ded8e --- /dev/null +++ b/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileCountTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.terrain + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TerrainTileCountTest { + + private val tinySeattleBox = GeoBounds(south = 47.6, west = -122.4, north = 47.61, east = -122.39) + private val continentalBox = GeoBounds(south = -60.0, west = -170.0, north = 60.0, east = 170.0) + + @Test + fun `a download that stays global counts one tile per zoom for a box inside one tile`() { + assertEquals(13L, terrainTileCount(tinySeattleBox, maxZoom = MapterhornEndpoints.GLOBAL_MAX_ZOOM)) + } + + @Test + fun `a box inside one regional archive adds the regional zooms`() { + // z13 and z14 cover 2 tiles apiece here, on top of the 13 global ones. + assertEquals(17L, terrainTileCount(tinySeattleBox, maxZoom = 14)) + } + + @Test + fun `a box with no regional archive never counts past the global tier`() { + val globalOnly = terrainTileCount(continentalBox, maxZoom = MapterhornEndpoints.GLOBAL_MAX_ZOOM) + + assertEquals(8_869_485L, globalOnly) + assertEquals(globalOnly, terrainTileCount(continentalBox, maxZoom = MapterhornEndpoints.REGIONAL_MAX_ZOOM)) + } + + @Test + fun `a negative maxZoom counts nothing`() { + assertEquals(0L, terrainTileCount(tinySeattleBox, maxZoom = -1)) + } + + @Test + fun `maxZoom 0 counts the one tile that covers the world`() { + assertEquals(1L, terrainTileCount(tinySeattleBox, maxZoom = 0)) + } +} diff --git a/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMathTest.kt b/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMathTest.kt index 446cea5408..31c2f7bb9d 100644 --- a/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMathTest.kt +++ b/feature/map-terrain/src/commonTest/kotlin/org/meshtastic/feature/map/terrain/TerrainTileMathTest.kt @@ -36,6 +36,44 @@ class TerrainTileMathTest { assertEquals(TileIndex(1, 1, 1), TerrainTileMath.tileAt(1, latitude = -45.0, longitude = 10.0)) // SE } + @Test + fun `tileAt matches published slippy map tiles`() { + assertEquals(TileIndex(10, 163, 395), TerrainTileMath.tileAt(10, latitude = 37.7749, longitude = -122.4194)) + assertEquals(TileIndex(16, 32745, 21794), TerrainTileMath.tileAt(16, latitude = 51.5007, longitude = -0.1246)) + } + + @Test + fun `lonLatAt returns a tile's northwest corner in fractional tile units`() { + val corner = TerrainTileMath.lonLatAt(zoom = 10, tileX = 163.0, tileY = 395.0) + assertEquals(-122.6953125, corner.longitude, absoluteTolerance = 1e-9) + assertEquals(37.99616267972812, corner.latitude, absoluteTolerance = 1e-9) + + val inside = TerrainTileMath.lonLatAt(zoom = 2, tileX = 3.25, tileY = 1.75) + assertEquals(112.5, inside.longitude, absoluteTolerance = 1e-9) + assertEquals(21.943045533438177, inside.latitude, absoluteTolerance = 1e-9) + } + + @Test + fun `worldFraction is the inverse of lonLatAt and pins the poles to the edges`() { + assertEquals(0.5 to 0.5, TerrainTileMath.worldFraction(latitude = 0.0, longitude = 0.0)) + + val (x, y) = TerrainTileMath.worldFraction(latitude = 37.7749, longitude = -122.4194) + val back = TerrainTileMath.lonLatAt(zoom = 0, tileX = x, tileY = y) + assertEquals(-122.4194, back.longitude, absoluteTolerance = 1e-9) + assertEquals(37.7749, back.latitude, absoluteTolerance = 1e-9) + + assertEquals( + 0.0, + TerrainTileMath.worldFraction(latitude = 90.0, longitude = 0.0).second, + absoluteTolerance = 1e-9, + ) + assertEquals( + 1.0, + TerrainTileMath.worldFraction(latitude = -90.0, longitude = 0.0).second, + absoluteTolerance = 1e-9, + ) + } + @Test fun `tilesAt covers a bbox's own corners inclusively`() { val bounds = GeoBounds(south = -1.0, west = -1.0, north = 1.0, east = 1.0) diff --git a/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt b/feature/map-terrain/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvmAndroid.kt similarity index 84% rename from feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt rename to feature/map-terrain/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvmAndroid.kt index 629bc76942..de798b9016 100644 --- a/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt +++ b/feature/map-terrain/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvmAndroid.kt @@ -21,19 +21,24 @@ import ch.poole.geo.pmtiles.HttpUrlConnectionChannel import ch.poole.geo.pmtiles.Reader import java.io.ByteArrayOutputStream import java.io.IOException -import java.net.URL +import java.net.URI import java.util.zip.GZIPInputStream -actual class TerrainTileFetcher actual constructor(pmtilesUrl: String) : AutoCloseable { +actual fun TerrainTileFetcher(pmtilesUrl: String): TerrainTileFetcher = PmtilesTerrainTileFetcher(pmtilesUrl) - private val reader: Reader = Reader(HttpUrlConnectionChannel(URL(pmtilesUrl))) +/** + * Backed by `ch.poole.geo.pmtiles:Reader` (MIT-licensed), the same library the base offline layer uses for Protomaps. + */ +private class PmtilesTerrainTileFetcher(pmtilesUrl: String) : TerrainTileFetcher { - actual fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? { + private val reader: Reader = Reader(HttpUrlConnectionChannel(URI(pmtilesUrl).toURL())) + + override fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? { val raw = reader.getTile(zoom, x, y) ?: return null return if (reader.tileCompression == Constants.COMPRESSION_GZIP) gunzip(raw) else raw } - actual override fun close() { + override fun close() { reader.close() } diff --git a/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt b/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt deleted file mode 100644 index 629bc76942..0000000000 --- a/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.map.terrain - -import ch.poole.geo.pmtiles.Constants -import ch.poole.geo.pmtiles.HttpUrlConnectionChannel -import ch.poole.geo.pmtiles.Reader -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.net.URL -import java.util.zip.GZIPInputStream - -actual class TerrainTileFetcher actual constructor(pmtilesUrl: String) : AutoCloseable { - - private val reader: Reader = Reader(HttpUrlConnectionChannel(URL(pmtilesUrl))) - - actual fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? { - val raw = reader.getTile(zoom, x, y) ?: return null - return if (reader.tileCompression == Constants.COMPRESSION_GZIP) gunzip(raw) else raw - } - - actual override fun close() { - reader.close() - } - - /** - * Bounded, not a bare `GZIPInputStream(...).readBytes()`: `download.mapterhorn.com` is a fixed first-party URL, but - * a compromised or MITM'd response is still a real defense-in-depth gap — an unbounded gunzip is a classic zip-bomb - * vector. A Terrarium tile decodes to at most 256×256×4 bytes (~256KB); [MAX_DECOMPRESSED_TILE_BYTES] is a generous - * multiple of that, not a tight fit. - */ - private fun gunzip(bytes: ByteArray): ByteArray { - GZIPInputStream(bytes.inputStream()).use { gzip -> - val buffer = ByteArray(GUNZIP_BUFFER_BYTES) - val output = ByteArrayOutputStream() - var totalRead = 0 - while (true) { - val read = gzip.read(buffer) - if (read == -1) break - totalRead += read - if (totalRead > MAX_DECOMPRESSED_TILE_BYTES) { - throw IOException("Decompressed terrain tile exceeds $MAX_DECOMPRESSED_TILE_BYTES bytes") - } - output.write(buffer, 0, read) - } - return output.toByteArray() - } - } - - private companion object { - private const val MAX_DECOMPRESSED_TILE_BYTES = 8 * 1024 * 1024 - private const val GUNZIP_BUFFER_BYTES = 8192 - } -} diff --git a/feature/map-terrain/src/nativeMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.native.kt b/feature/map-terrain/src/nativeMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.native.kt index 908f51a73a..2783e14154 100644 --- a/feature/map-terrain/src/nativeMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.native.kt +++ b/feature/map-terrain/src/nativeMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.native.kt @@ -20,12 +20,12 @@ package org.meshtastic.feature.map.terrain * Placeholder Kotlin/Native implementation — same reason as [decodeTerrariumTile]'s nativeMain actual: this module has * no iOS surface, but the shared KMP convention plugin adds Kotlin/Native targets to every module regardless. */ -actual class TerrainTileFetcher actual constructor(pmtilesUrl: String) : AutoCloseable { +actual fun TerrainTileFetcher(pmtilesUrl: String): TerrainTileFetcher = object : TerrainTileFetcher { - actual fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? = throw NotImplementedError( + override fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? = throw NotImplementedError( "Terrain tile fetching is not implemented for Kotlin/Native (iOS). " + "This module has no iOS surface; use the Android or JVM implementations instead.", ) - actual override fun close() = Unit + override fun close() = Unit } diff --git a/feature/map/README.md b/feature/map/README.md index 3010765b84..a5341edfde 100644 --- a/feature/map/README.md +++ b/feature/map/README.md @@ -23,7 +23,7 @@ All providers are injected via `CompositionLocal` — in `MainActivity.kt` on An ### Shared ViewModels (in `commonMain`) - **`BaseMapViewModel`** — Core contract for all map state management, node markers, camera positions, and traceroute node selection logic (`TracerouteNodeSelection`, `tracerouteNodeSelection()`). -- **`NodeMapViewModel`** — Shared logic for per-node map views (track display, position history). +- **`NodeMapViewModel`**: resolves a node number to its `Node` for the Google flavor's embedded node-track map. ### Shared Logic (in `commonMain`) @@ -63,15 +63,10 @@ Rules both renderers must agree on, so a behaviour difference between the flavor ```mermaid graph TB :feature:map[map]:::kmp-feature - :feature:map -.-> :core:data - :feature:map -.-> :core:database - :feature:map -.-> :core:datastore :feature:map -.-> :core:model :feature:map -.-> :core:navigation :feature:map -.-> :core:network - :feature:map -.-> :core:prefs :feature:map -.-> :core:repository - :feature:map -.-> :core:service :feature:map -.-> :core:resources :feature:map -.-> :core:ui :feature:map -.-> :core:di diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index a865d82149..e9544ebebd 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -16,6 +16,7 @@ */ plugins { alias(libs.plugins.meshtastic.kmp.feature) + alias(libs.plugins.meshtastic.kmp.jvm.android) alias(libs.plugins.meshtastic.kotlinx.serialization) } @@ -28,20 +29,14 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.kotlinx.collections.immutable) // KML import parses through the same xmlutil the app already resolves for CoT XML. implementation(libs.xmlutil.core) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) implementation(projects.core.model) implementation(projects.core.navigation) // NetworkRepository backs the offline-basemap auto-fallback (see OfflineFallback.kt). implementation(projects.core.network) - implementation(projects.core.prefs) implementation(projects.core.repository) implementation(libs.meshtastic.protobufs) - implementation(projects.core.service) implementation(projects.core.resources) implementation(projects.core.ui) implementation(projects.core.di) diff --git a/feature/map/detekt-baseline.xml b/feature/map/detekt-baseline.xml index 448b16e8b9..b87d13e9df 100644 --- a/feature/map/detekt-baseline.xml +++ b/feature/map/detekt-baseline.xml @@ -3,5 +3,13 @@ ComposableParamOrder:MapScreen.kt:@Composable fun MapScreen + LongParameterList:BaseMapViewModel.kt:BaseMapViewModel + LongParameterList:SharedMapViewModel.kt:SharedMapViewModel + UnnecessaryLaunchedEffect:EditWaypointDialog.kt:LaunchedEffect + UnnecessaryLaunchedEffect:SitePlannerSheet.kt:LaunchedEffect + UnusedPrivateProperty:BaseMapViewModel.kt:BaseMapViewModel$private val radioConfigRepository: RadioConfigRepository + UseOrEmpty:BaseMapViewModel.kt:tracerouteOverlay?.relatedNodeNums ?: emptySet() + UseOrEmpty:CustomTileProviderManager.kt:config?.name ?: "" + UseOrEmpty:CustomTileProviderManager.kt:config?.urlTemplate ?: "" diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.android.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.android.kt new file mode 100644 index 0000000000..12671efeec --- /dev/null +++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.android.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.tiles + +import android.security.NetworkSecurityPolicy + +/** Answers from the app's network security config, the same check the HTTP stacks apply when they connect. */ +actual fun isCleartextPermitted(host: String): Boolean = + NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host) diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 38ae55a33f..0e45c88f16 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -112,6 +112,21 @@ open class BaseMapViewModel( .map { nodes -> nodes.filterNot { node -> node.isIgnored } } .stateInWhileSubscribed(initialValue = emptyList()) + // Set by the map provider because this SavedStateHandle is not the Navigation 3 entry's route state. + private val sitePlannerRequestState = SitePlannerRequestState(nodeRepository.nodeDBbyNum) + + /** The node a Site Planner route asked the map to open, until the map consumes it. */ + val sitePlannerRequest: StateFlow = + sitePlannerRequestState.request.stateInWhileSubscribed(initialValue = null) + + fun setSitePlannerNodeNum(nodeNum: Int?) { + sitePlannerRequestState.setNodeNum(nodeNum) + } + + fun consumeSitePlannerRequest(nodeNum: Int) { + sitePlannerRequestState.consume(nodeNum) + } + val waypoints: StateFlow> = packetRepository .getWaypoints() @@ -138,8 +153,9 @@ open class BaseMapViewModel( fun toggleShowWaypointsOnMap() = mapPrefs.updateMapFilters { it.copy(showWaypoints = !it.showWaypoints) } - fun toggleShowPrecisionCircleOnMap() = - mapPrefs.updateMapFilters { it.copy(showPrecisionCircle = !it.showPrecisionCircle) } + fun toggleShowPrecisionCircleOnMap() = mapPrefs.updateMapFilters { + it.copy(showPrecisionCircle = !it.showPrecisionCircle) + } fun toggleOnlyOnline() = mapPrefs.updateMapFilters { it.copy(onlyOnline = !it.onlyOnline) } @@ -183,11 +199,13 @@ open class BaseMapViewModel( fun clearExcludedRoles() = mapPrefs.updateMapFilters { it.copy(excludedRoles = emptySet()) } - fun setLastHeardFilter(filter: LastHeardFilter) = - mapPrefs.updateMapFilters { it.copy(lastHeardSeconds = filter.seconds) } + fun setLastHeardFilter(filter: LastHeardFilter) = mapPrefs.updateMapFilters { + it.copy(lastHeardSeconds = filter.seconds) + } - fun setLastHeardTrackFilter(filter: LastHeardFilter) = - mapPrefs.updateMapFilters { it.copy(lastHeardTrackSeconds = filter.seconds) } + fun setLastHeardTrackFilter(filter: LastHeardFilter) = mapPrefs.updateMapFilters { + it.copy(lastHeardTrackSeconds = filter.seconds) + } open fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.NodeAddress.ID_BROADCAST) diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRequestState.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SitePlannerRequestState.kt similarity index 98% rename from androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRequestState.kt rename to feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SitePlannerRequestState.kt index c3e5329912..e20e963fcd 100644 --- a/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRequestState.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SitePlannerRequestState.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.map +package org.meshtastic.feature.map import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/TrackRuns.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/TrackRuns.kt new file mode 100644 index 0000000000..38825d3016 --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/TrackRuns.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +import org.meshtastic.core.common.util.latLongToMeter +import org.meshtastic.proto.Position + +/** Wider than a stationary fix's GPS jitter, narrower than a walked block. */ +const val TRACK_STOP_RADIUS_METERS = 25.0 + +private const val DEG_PER_UNIT = 1e-7 + +/** Consecutive fixes drawn as one point: the newest fix of the run, and the time the run began. */ +data class TrackRun(val position: Position, val firstTime: Int) { + fun covers(time: Int): Boolean = time in firstTime..position.time +} + +/** + * Collapses each run of consecutive fixes that stays within [radiusMeters] of the run's first fix into one [TrackRun]. + * [positions] must be oldest-first, and so is the result. + * + * Distance is measured from the run's first fix, not the previous one, so a slow drift still breaks into new points. A + * fix missing either ordinate is never merged. + */ +fun mergeStationaryRuns(positions: List, radiusMeters: Double = TRACK_STOP_RADIUS_METERS): List { + val runs = ArrayList(positions.size) + var runStart: Position? = null + for (position in positions) { + val start = runStart + if (start != null && start.isWithin(position, radiusMeters)) { + runs[runs.lastIndex] = runs.last().copy(position = position) + } else { + runs += TrackRun(position = position, firstTime = position.time) + runStart = position + } + } + return runs +} + +private fun Position.isWithin(other: Position, radiusMeters: Double): Boolean { + val latA = latitude_i + val lonA = longitude_i + val latB = other.latitude_i + val lonB = other.longitude_i + return latA != null && + lonA != null && + latB != null && + lonB != null && + latLongToMeter(latA * DEG_PER_UNIT, lonA * DEG_PER_UNIT, latB * DEG_PER_UNIT, lonB * DEG_PER_UNIT) <= + radiusMeters +} diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt index 2b3909ac3e..37e9d022e7 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt @@ -69,6 +69,7 @@ import org.meshtastic.core.resources.save import org.meshtastic.core.resources.show_layer import org.meshtastic.core.resources.url import org.meshtastic.core.resources.url_cannot_be_empty +import org.meshtastic.core.resources.url_http_localhost_only import org.meshtastic.core.resources.url_must_be_http import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.icon.CellTower @@ -80,6 +81,7 @@ import org.meshtastic.core.ui.icon.Visibility import org.meshtastic.core.ui.icon.VisibilityOff import org.meshtastic.feature.map.layers.LayerType import org.meshtastic.feature.map.layers.MapLayerItem +import org.meshtastic.feature.map.layers.isRefusedCleartextLayerUrl import org.meshtastic.feature.map.layers.isValidNetworkLayerUrl import org.meshtastic.feature.map.layers.opacityOf @@ -271,6 +273,7 @@ fun AddNetworkLayerDialog(onDismiss: () -> Unit, onConfirm: (String, String) -> val emptyNameError = stringResource(Res.string.name_cannot_be_empty) val emptyUrlError = stringResource(Res.string.url_cannot_be_empty) val invalidUrlError = stringResource(Res.string.url_must_be_http) + val httpLocalhostOnlyError = stringResource(Res.string.url_http_localhost_only) // Validated here, not just in the store: the store's error return is dropped by two of its three callers, // so this dialog is the one place the user can be told. Same rules as [isValidNetworkLayerUrl]. @@ -279,6 +282,7 @@ fun AddNetworkLayerDialog(onDismiss: () -> Unit, onConfirm: (String, String) -> urlError = when { url.isBlank() -> emptyUrlError + isRefusedCleartextLayerUrl(url.trim()) -> httpLocalhostOnlyError !isValidNetworkLayerUrl(url.trim()) -> invalidUrlError else -> null } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManager.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManager.kt index 9e7fbaa018..ad709573ed 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManager.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManager.kt @@ -55,6 +55,7 @@ import org.meshtastic.core.resources.no_custom_tile_sources_found import org.meshtastic.core.resources.provider_name_exists import org.meshtastic.core.resources.save import org.meshtastic.core.resources.url_cannot_be_empty +import org.meshtastic.core.resources.url_http_localhost_only import org.meshtastic.core.resources.url_must_contain_placeholders import org.meshtastic.core.resources.url_template import org.meshtastic.core.resources.url_template_hint @@ -63,6 +64,7 @@ import org.meshtastic.core.ui.icon.Delete import org.meshtastic.core.ui.icon.Edit import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.map.tiles.CustomTileProviderConfig +import org.meshtastic.feature.map.tiles.isRefusedCleartextTileUrl import org.meshtastic.feature.map.tiles.isValidTileUrlTemplate @Suppress("LongMethod", "LongParameterList") @@ -188,10 +190,11 @@ private fun AddEditCustomTileProviderDialog( val providerNameExistsError = stringResource(Res.string.provider_name_exists) val urlCannotBeEmptyError = stringResource(Res.string.url_cannot_be_empty) val urlMustContainPlaceholdersError = stringResource(Res.string.url_must_contain_placeholders) + val httpLocalhostOnlyError = stringResource(Res.string.url_http_localhost_only) fun validateAndSave() { nameError = validateName(name, providers, config?.id, emptyNameError, providerNameExistsError) - urlError = validateUrl(url, urlCannotBeEmptyError, urlMustContainPlaceholdersError) + urlError = validateUrl(url, urlCannotBeEmptyError, urlMustContainPlaceholdersError, httpLocalhostOnlyError) if (nameError == null && urlError == null) { onSave( (config ?: CustomTileProviderConfig(name = name, urlTemplate = url)) @@ -252,8 +255,14 @@ private fun validateName( else -> null } -private fun validateUrl(url: String, emptyUrlError: String, missingPlaceholdersError: String): String? = when { +private fun validateUrl( + url: String, + emptyUrlError: String, + missingPlaceholdersError: String, + httpLocalhostOnlyError: String, +): String? = when { url.isBlank() -> emptyUrlError - !url.isValidTileUrlTemplate(requireHttps = false) -> missingPlaceholdersError + url.isRefusedCleartextTileUrl() -> httpLocalhostOnlyError + !url.isValidTileUrlTemplate() -> missingPlaceholdersError else -> null } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFitPadding.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFitPadding.kt new file mode 100644 index 0000000000..3fd9debe12 --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFitPadding.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.dp + +/** + * How far the mesh map keeps each framed node from the edges, so the node's chip clears the map's own chrome. + * + * Asymmetric because the chrome is: [MapControlsOverlay] reaches 72dp down from the top, the zoom pair reaches 72dp in + * from the lower trailing corner, and the logo and attribution run along the foot. A node chip is about 64 by 28dp, + * centred on its point, so each edge also leaves half a chip. + */ +val MeshMapFitPadding = PaddingValues(start = 40.dp, top = 88.dp, end = 104.dp, bottom = 56.dp) diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeoJson.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeoJson.kt index d822cfd0cc..ab7bd7a962 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeoJson.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeoJson.kt @@ -56,18 +56,19 @@ internal fun KmlGeometry.toFeature(placemark: Placemark, style: KmlStyle?): Stri */ internal fun String.toCssColor(): Pair? { val bytes = - removePrefix("#").takeIf { it.length == KML_COLOR_LENGTH }?.chunked(BYTE_CHARS)?.map { it.toIntOrNull(HEX) } - return if (bytes == null || bytes.any { it == null }) { - null - } else { - val opacity = bytes[ALPHA]!!.toDouble() / MAX_CHANNEL - // Rendered digit by digit rather than through a format string. This is JSON, not display text, and a - // locale-aware `%f` writes `"fill-opacity":0,498` on a comma-decimal device — invalid JSON, which makes - // MapLibre reject the whole converted file so every KML import silently draws nothing. The previous - // implementation pinned Locale.US to avoid that; building the text by hand cannot regress into it, and works - // the same on every platform. - "#${bytes[RED]!!.hexByte()}${bytes[GREEN]!!.hexByte()}${bytes[BLUE]!!.hexByte()}" to opacity.toFixed() - } + removePrefix("#") + .takeIf { it.length == KML_COLOR_LENGTH } + ?.chunked(BYTE_CHARS) + ?.map { it.toIntOrNull(HEX) } + ?.takeIf { parsed -> parsed.none { it == null } } + ?.filterNotNull() ?: return null + val opacity = bytes[ALPHA].toDouble() / MAX_CHANNEL + // Rendered digit by digit rather than through a format string. This is JSON, not display text, and a + // locale-aware `%f` writes `"fill-opacity":0,498` on a comma-decimal device. That is invalid JSON, which makes + // MapLibre reject the whole converted file so every KML import silently draws nothing. The previous + // implementation pinned Locale.US to avoid that; building the text by hand cannot regress into it, and works + // the same on every platform. + return "#${bytes[RED].hexByte()}${bytes[GREEN].hexByte()}${bytes[BLUE].hexByte()}" to opacity.toFixed() } /** Minimal JSON string escaping — KML descriptions routinely carry quotes, newlines and CDATA-wrapped HTML. */ diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeometry.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeometry.kt index 644ec18f9e..44654faecd 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeometry.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/kml/KmlGeometry.kt @@ -80,7 +80,7 @@ private fun List.isCounterClockwise(): Boolean { */ private fun List.splitAtAntimeridian(): List> { val segments = mutableListOf>() - var current = mutableListOf(first()) + val current = mutableListOf(first()) for (next in drop(1)) { val previous = current.last() @@ -89,8 +89,9 @@ private fun List.splitAtAntimeridian(): List> { val exit = if (delta > 0) -HALF_TURN else HALF_TURN val latitude = crossingLatitude(previous, next, exit) current += GeoPosition(exit, latitude) - segments += current - current = mutableListOf(GeoPosition(-exit, latitude)) + segments += current.toList() + current.clear() + current += GeoPosition(-exit, latitude) } current += next } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/layers/MapLayersManager.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/layers/MapLayersManager.kt index d33c925a79..6f43b640d3 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/layers/MapLayersManager.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/layers/MapLayersManager.kt @@ -37,6 +37,7 @@ import okio.Path import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.MapPrefs +import org.meshtastic.feature.map.tiles.isCleartextPermitted /** * Owner of the imported map-layer list, its on-disk persistence, and the import plumbing. @@ -97,6 +98,8 @@ class MapLayersManager( if (_mapLayers.value.isNotEmpty()) { Logger.withTag(TAG).i("Loaded ${_mapLayers.value.size} persisted map layers.") } + } catch (e: CancellationException) { + throw e } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.withTag(TAG).e(e) { "Error loading persisted map layers" } _mapLayers.value = emptyList() @@ -292,16 +295,35 @@ internal const val LAYERS_DIR = "map_layers" * * The scheme check is on the string, not the parsed protocol — Ktor's [Url] defaults a missing scheme to `http`, so * `example.com/map.kml` would parse as valid and then be stored as a string nothing can fetch. Shared with the - * add-layer dialog so the form and the store cannot disagree about what is acceptable. + * add-layer dialog so the form and the store cannot disagree about what is acceptable. Plain http is accepted only for + * a host the platform allows it to. */ -fun isValidNetworkLayerUrl(url: String): Boolean { - val hasScheme = url.startsWith("http://", ignoreCase = true) || url.startsWith("https://", ignoreCase = true) - if (!hasScheme) return false +fun isValidNetworkLayerUrl( + url: String, + cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted, +): Boolean { + val parsed = parseNetworkLayerUrl(url) ?: return false + return !parsed.isHttp || cleartextPermitted(parsed.url.host) +} + +/** Whether [url] is a parseable http URL whose host the platform refuses plain http to. */ +fun isRefusedCleartextLayerUrl( + url: String, + cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted, +): Boolean { + val parsed = parseNetworkLayerUrl(url) ?: return false + return parsed.isHttp && !cleartextPermitted(parsed.url.host) +} + +private class ParsedLayerUrl(val url: Url, val isHttp: Boolean) + +private fun parseNetworkLayerUrl(url: String): ParsedLayerUrl? { + val isHttp = url.startsWith("http://", ignoreCase = true) + if (!isHttp && !url.startsWith("https://", ignoreCase = true)) return null return try { - Url(url) - true + ParsedLayerUrl(Url(url), isHttp) } catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception) { - false + null } } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 6204dc5ecf..e8d219ba2a 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -19,31 +19,16 @@ package org.meshtastic.feature.map.node import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.toList import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.model.MeshLog -import org.meshtastic.core.repository.MapPrefs -import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.ui.util.toPosition import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed -import org.meshtastic.proto.PortNum -import org.meshtastic.proto.Position @KoinViewModel -class NodeMapViewModel( - savedStateHandle: SavedStateHandle, - nodeRepository: NodeRepository, - meshLogRepository: MeshLogRepository, - private val mapPrefs: MapPrefs, -) : ViewModel() { +class NodeMapViewModel(savedStateHandle: SavedStateHandle, nodeRepository: NodeRepository) : ViewModel() { private val destNumFromRoute = savedStateHandle.get("destNum") private val manualDestNum = MutableStateFlow(null) @@ -59,28 +44,4 @@ class NodeMapViewModel( .flatMapLatest { destNum -> nodeRepository.nodeDBbyNum.mapLatest { it[destNum] } } .distinctUntilChanged() .stateInWhileSubscribed(initialValue = null) - - private val ourNodeNumFlow = nodeRepository.myNodeInfo.map { it?.myNodeNum }.distinctUntilChanged() - - val positionLogs: StateFlow> = - combine(ourNodeNumFlow, destNumFlow) { ourNodeNum, destNum -> - if (destNum == ourNodeNum) MeshLog.NODE_NUM_LOCAL else destNum - } - .distinctUntilChanged() - .flatMapLatest { logId -> - meshLogRepository.getMeshPacketsFrom(logId, PortNum.POSITION_APP.value).map { packets -> - packets - .mapNotNull { it.toPosition() } - .asFlow() - .distinctUntilChanged { old, new -> - old.time == new.time || - (old.latitude_i == new.latitude_i && old.longitude_i == new.longitude_i) - } - .toList() - } - } - .stateInWhileSubscribed(initialValue = emptyList()) - - val mapStyleId: Int - get() = mapPrefs.mapStyle.value } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.kt new file mode 100644 index 0000000000..3f18a81d49 --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.tiles + +/** + * Whether this platform will open a plain http connection to [host]. A URL validator that accepts http where this is + * false saves a tile source or layer that can never load. + */ +expect fun isCleartextPermitted(host: String): Boolean diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfig.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfig.kt index f63ac37325..d3e407e89c 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfig.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfig.kt @@ -43,9 +43,15 @@ data class CustomTileProviderConfig( * A private/link-local host blocklist is intentionally omitted: the user supplies the tile endpoint, requests carry no * Meshtastic-held credentials, and client-side tile GETs make that SSRF shape an accepted low-risk case. */ -fun String.isValidTileUrlTemplate(requireHttps: Boolean): Boolean { +fun String.isValidTileUrlTemplate(cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted): Boolean { val resolved = resolvedForValidation() ?: return false - return resolved.hasAcceptedScheme(requireHttps) && resolved.hasUsableAuthority() + return resolved.hasUsableAuthority() && resolved.hasAcceptedScheme(cleartextPermitted) +} + +/** Whether this is an otherwise usable http template whose host the platform refuses plain http to. */ +fun String.isRefusedCleartextTileUrl(cleartextPermitted: (host: String) -> Boolean = ::isCleartextPermitted): Boolean { + val resolved = resolvedForValidation() ?: return false + return resolved.scheme() == "http" && resolved.hasUsableAuthority() && !cleartextPermitted(resolved.host()) } /** @@ -66,16 +72,30 @@ private fun String.resolvedForValidation(): String? { return resolved.takeIf { hasPlaceholders && '{' !in it && '}' !in it && it.none(Char::isWhitespace) } } -private fun String.hasAcceptedScheme(requireHttps: Boolean): Boolean { - val scheme = substringBefore(SCHEME_SEPARATOR, missingDelimiterValue = "").lowercase() - return if (requireHttps) scheme == "https" else scheme == "http" || scheme == "https" +private fun String.hasAcceptedScheme(cleartextPermitted: (host: String) -> Boolean): Boolean = when (scheme()) { + "https" -> true + "http" -> cleartextPermitted(host()) + else -> false } /** A host, no fragment, and no credentials — those would be persisted in the clear and sent with every tile. */ private fun String.hasUsableAuthority(): Boolean { - val afterScheme = substringAfter(SCHEME_SEPARATOR) - val authority = afterScheme.substringBefore('/').substringBefore('?') - return '#' !in afterScheme && '@' !in authority && authority.substringBefore(':').isNotBlank() + val authority = authority() + return '#' !in substringAfter(SCHEME_SEPARATOR) && '@' !in authority && host().isNotBlank() +} + +private fun String.scheme(): String = substringBefore(SCHEME_SEPARATOR, missingDelimiterValue = "").lowercase() + +private fun String.authority(): String = substringAfter(SCHEME_SEPARATOR).substringBefore('/').substringBefore('?') + +/** The authority without its port; an IPv6 literal loses its brackets, and an unterminated one has no host. */ +private fun String.host(): String { + val authority = authority() + return if (authority.startsWith('[')) { + if (']' !in authority) "" else authority.substringAfter('[').substringBefore(']') + } else { + authority.substringBefore(':') + } } private const val SCHEME_SEPARATOR = "://" diff --git a/androidApp/src/testFdroid/kotlin/org/meshtastic/app/map/MapViewModelSitePlannerRequestTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelSitePlannerTest.kt similarity index 71% rename from androidApp/src/testFdroid/kotlin/org/meshtastic/app/map/MapViewModelSitePlannerRequestTest.kt rename to feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelSitePlannerTest.kt index 9d264275b1..d10767a36a 100644 --- a/androidApp/src/testFdroid/kotlin/org/meshtastic/app/map/MapViewModelSitePlannerRequestTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelSitePlannerTest.kt @@ -14,15 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.map +package org.meshtastic.feature.map -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import dev.mokkery.MockMode import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.mock -import io.ktor.client.HttpClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -31,12 +28,6 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import okio.Path.Companion.toOkioPath -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.Node import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.PacketRepository @@ -46,67 +37,44 @@ import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.FakeNotificationPrefs import org.meshtastic.core.testing.FakeRadioConfigRepository import org.meshtastic.core.testing.FakeRadioController -import org.meshtastic.feature.map.layers.MapLayersManager -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import kotlin.io.path.createTempDirectory +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -import java.nio.file.Path as NioPath @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = android.app.Application::class) -class MapViewModelSitePlannerRequestTest { +class BaseMapViewModelSitePlannerTest { private val testDispatcher = StandardTestDispatcher() private val nodeRepository = FakeNodeRepository() - private val packetRepository = mock(MockMode.autofill) - private val networkRepository = mock(MockMode.autofill) - private val mapPrefs = FakeMapPrefs() + private val packetRepository: PacketRepository = mock() + private val networkRepository: NetworkRepository = mock() private val firstNode = Node(num = 11) private val secondNode = Node(num = 22) - private lateinit var httpClient: HttpClient - private lateinit var layersDir: NioPath - private lateinit var mapLayersManager: MapLayersManager - private lateinit var viewModel: MapViewModel + private lateinit var viewModel: BaseMapViewModel - @Before + @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) every { packetRepository.getWaypoints() } returns flowOf(emptyList()) every { networkRepository.networkAvailable } returns flowOf(true) - httpClient = HttpClient() - layersDir = createTempDirectory("map-layers") - mapLayersManager = - MapLayersManager( - dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher), - httpClient = httpClient, - mapPrefs = mapPrefs, - // The real location reads a global application context this test never installs. - layersDir = layersDir.toOkioPath(), - ) - nodeRepository.setNodes(listOf(firstNode, secondNode)) viewModel = - MapViewModel( - mapPrefs = mapPrefs, - packetRepository = packetRepository, + BaseMapViewModel( + mapPrefs = FakeMapPrefs(), nodeRepository = nodeRepository, + packetRepository = packetRepository, radioController = FakeRadioController(), radioConfigRepository = FakeRadioConfigRepository(), notificationPrefs = FakeNotificationPrefs(), - mapLayersManager = mapLayersManager, - savedStateHandle = SavedStateHandle(), localeUnitsProvider = FakeLocaleUnitsProvider(), networkRepository = networkRepository, ) } - @After + @AfterTest fun tearDown() { - httpClient.close() - layersDir.toFile().deleteRecursively() Dispatchers.resetMain() } diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/LastHeardFilterTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/LastHeardFilterTest.kt index 052e85da96..40f9889bfa 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/LastHeardFilterTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/LastHeardFilterTest.kt @@ -19,7 +19,6 @@ package org.meshtastic.feature.map import kotlin.test.Test import kotlin.test.assertEquals -@Suppress("MagicNumber") class LastHeardFilterTest { @Test diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TrackRunsTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TrackRunsTest.kt new file mode 100644 index 0000000000..33318cb907 --- /dev/null +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TrackRunsTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +import org.meshtastic.proto.Position +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TrackRunsTest { + + // One degree of latitude is about 111 km, so 1e-4 degrees is about 11 m. + private fun fix(time: Int, latitude: Double?, longitude: Double? = 0.0) = Position.Builder() + .also { builder -> + builder.latitude_i = latitude?.let { (it * 1e7).toInt() } + builder.longitude_i = longitude?.let { (it * 1e7).toInt() } + builder.time = time + } + .build() + + @Test + fun `an empty track has no runs`() { + assertEquals(emptyList(), mergeStationaryRuns(emptyList())) + } + + @Test + fun `a node that never moves is one point at its newest fix`() { + val track = (0 until 1440).map { minute -> fix(time = 1_000 + minute * 60, latitude = 45.0) } + + val runs = mergeStationaryRuns(track) + + assertEquals(1, runs.size) + assertEquals(track.last(), runs.single().position) + assertEquals(1_000, runs.single().firstTime) + } + + @Test + fun `jitter inside the radius stays one point`() { + val track = listOf(fix(1, 45.0), fix(2, 45.0001), fix(3, 44.9999), fix(4, 45.00015)) + + assertEquals(1, mergeStationaryRuns(track).size) + } + + @Test + fun `moving keeps every fix`() { + val track = (0 until 10).map { step -> fix(time = step, latitude = 45.0 + step * 0.001) } + + assertEquals(track, mergeStationaryRuns(track).map { it.position }) + } + + @Test + fun `a slow drift breaks into new points`() { + // Each step is about 11 m, under the radius, but the run is measured from its first fix. + val track = (0 until 10).map { step -> fix(time = step, latitude = 45.0 + step * 0.0001) } + + val runs = mergeStationaryRuns(track) + + assertTrue(runs.size > 1) + assertEquals(track.last(), runs.last().position) + } + + @Test + fun `a stop between two legs keeps both legs`() { + val track = listOf(fix(1, 45.0), fix(2, 45.01), fix(3, 45.01), fix(4, 45.01), fix(5, 45.02)) + + val runs = mergeStationaryRuns(track) + + assertEquals(listOf(1, 4, 5), runs.map { it.position.time }) + assertEquals(listOf(1, 2, 5), runs.map { it.firstTime }) + } + + @Test + fun `a fix with no latitude is never merged`() { + val track = listOf(fix(1, 45.0), fix(2, null), fix(3, null)) + + assertEquals(3, mergeStationaryRuns(track).size) + } + + @Test + fun `a run covers every fix it absorbed and nothing outside it`() { + val runs = mergeStationaryRuns(listOf(fix(10, 45.0), fix(20, 45.0), fix(30, 45.0), fix(40, 45.01))) + val stop = runs.first() + + assertTrue(stop.covers(10)) + assertTrue(stop.covers(20)) + assertTrue(stop.covers(30)) + assertFalse(stop.covers(9)) + assertFalse(stop.covers(40)) + } +} diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt index bb819d3c7d..4763fb400f 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt @@ -65,7 +65,7 @@ class KmlGroundOverlayTest { } @Test - fun `an overlay-only document is not "nothing mappable"`() { + fun `an overlay-only document still counts as mappable`() { val result = KmlToGeoJson.convertDocument( """ diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/layers/NetworkLayerUrlTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/layers/NetworkLayerUrlTest.kt new file mode 100644 index 0000000000..2a1ca739ea --- /dev/null +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/layers/NetworkLayerUrlTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map.layers + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NetworkLayerUrlTest { + private val loopbackOnly: (String) -> Boolean = { it == "localhost" || it == "127.0.0.1" } + + @Test + fun `plain http to a host the platform refuses is invalid and reported as refused cleartext`() { + val url = "http://example.org/map.kml" + assertFalse(isValidNetworkLayerUrl(url, loopbackOnly)) + assertTrue(isRefusedCleartextLayerUrl(url, loopbackOnly)) + } + + @Test + fun `plain http to a host the platform allows is valid`() { + assertTrue(isValidNetworkLayerUrl("http://localhost:8080/map.geojson", loopbackOnly)) + assertTrue(isValidNetworkLayerUrl("http://127.0.0.1/map.kml", loopbackOnly)) + assertFalse(isRefusedCleartextLayerUrl("http://localhost:8080/map.geojson", loopbackOnly)) + } + + @Test + fun `https never consults the cleartext policy`() { + val failIfAsked: (String) -> Boolean = { error("asked about $it") } + assertTrue(isValidNetworkLayerUrl("https://example.org/map.kml", failIfAsked)) + assertFalse(isRefusedCleartextLayerUrl("https://example.org/map.kml", failIfAsked)) + } + + @Test + fun `the cleartext policy is asked about the host alone`() { + val asked = mutableListOf() + isValidNetworkLayerUrl("HTTP://Example.org:8080/map.kml?x=1") { host -> false.also { asked += host } } + assertEquals(1, asked.size) + assertEquals("example.org", asked.single().lowercase()) + } + + @Test + fun `a url without an explicit scheme is invalid but not reported as refused cleartext`() { + assertFalse(isValidNetworkLayerUrl("example.org/map.kml") { true }) + assertFalse(isRefusedCleartextLayerUrl("example.org/map.kml") { false }) + } +} diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfigTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfigTest.kt index be11e6c06c..60ce5f8dff 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfigTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/tiles/CustomTileProviderConfigTest.kt @@ -17,50 +17,87 @@ package org.meshtastic.feature.map.tiles import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class CustomTileProviderConfigTest { + // Every http case passes its policy explicitly: the Android actual needs a real framework and these also run as + // host tests. + private val cleartextAllowed: (String) -> Boolean = { true } + private val cleartextRefused: (String) -> Boolean = { false } + @Test - fun `Google-compatible validation retains HTTP support`() { - assertTrue("http://tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) - assertTrue("https://{s}.example.org/{Z}/{X}/{Y}.jpg".isValidTileUrlTemplate(requireHttps = false)) + fun `http is accepted where the platform permits plain http to the host`() { + assertTrue("http://tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("http://tiles.example.org/{z}/{x}/{y}.png".isRefusedCleartextTileUrl(cleartextAllowed)) } @Test - fun `HTTPS can be required`() { - assertTrue("https://tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = true)) - assertFalse("http://tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = true)) + fun `http is refused where the platform refuses plain http to the host`() { + assertFalse("http://tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextRefused)) + assertTrue("http://tiles.example.org/{z}/{x}/{y}.png".isRefusedCleartextTileUrl(cleartextRefused)) + } + + @Test + fun `https never consults the cleartext policy`() { + val failIfAsked: (String) -> Boolean = { error("asked about $it") } + assertTrue("https://{s}.example.org/{Z}/{X}/{Y}.jpg".isValidTileUrlTemplate(failIfAsked)) + assertFalse("https://tiles.example.org/{z}/{x}/{y}.png".isRefusedCleartextTileUrl(failIfAsked)) + } + + @Test + fun `the cleartext policy is asked about the bare host`() { + val asked = mutableListOf() + val recordAndAllow: (String) -> Boolean = { host -> true.also { asked += host } } + + assertTrue("http://127.0.0.1:8080/{z}/{x}/{y}.png".isValidTileUrlTemplate(recordAndAllow)) + assertTrue("http://[::1]:8080/{z}/{x}/{y}.png".isValidTileUrlTemplate(recordAndAllow)) + assertTrue("HTTP://localhost/{z}/{x}/{y}.png?v=1".isValidTileUrlTemplate(recordAndAllow)) + + assertEquals(listOf("127.0.0.1", "::1", "localhost"), asked) + } + + @Test + fun `an unterminated IPv6 literal has no host`() { + assertFalse("http://[::1/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("https://[::1/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("http://[::1/{z}/{x}/{y}.png".isRefusedCleartextTileUrl(cleartextRefused)) + } + + @Test + fun `a malformed http template is not reported as refused cleartext`() { + // The form reports these as malformed instead, which is the fix the user actually needs. + assertFalse("http://tiles.example.org/{z}/{x}.png".isRefusedCleartextTileUrl(cleartextRefused)) + assertFalse("http://token@tiles.example.org/{z}/{x}/{y}.png".isRefusedCleartextTileUrl(cleartextRefused)) } @Test fun `a template missing any of the three coordinates is rejected`() { - assertFalse("https://tiles.example.org/{z}/{x}.png".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("https://tiles.example.org/static.png".isValidTileUrlTemplate(requireHttps = false)) + assertFalse("https://tiles.example.org/{z}/{x}.png".isValidTileUrlTemplate()) + assertFalse("https://tiles.example.org/static.png".isValidTileUrlTemplate()) } @Test fun `validation refuses what is not an http url at all`() { // These are the shapes a hand-written parser gets wrong: no scheme, a scheme we do not fetch, and whitespace // that a URL type would have thrown on. - assertFalse("tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("file:///tiles/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("javascript:alert('{z}{x}{y}')".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("https://tiles example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) + assertFalse("tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("file:///tiles/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("javascript:alert('{z}{x}{y}')".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("https://tiles example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) } @Test fun `a port and a query string are both fine`() { - assertTrue("https://tiles.example.org:8443/{z}/{x}/{y}.png?v=2".isValidTileUrlTemplate(requireHttps = false)) + assertTrue("https://tiles.example.org:8443/{z}/{x}/{y}.png?v=2".isValidTileUrlTemplate()) } @Test - fun `Google-compatible validation rejects unsafe and unresolved templates`() { - assertFalse("http://token@tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("http:///tiles/{z}/{x}/{y}.png".isValidTileUrlTemplate(requireHttps = false)) - assertFalse("http://tiles.example.org/static#{z}/{x}/{y}".isValidTileUrlTemplate(requireHttps = false)) - assertFalse( - "http://tiles.example.org/{z}/{x}/{y}.png?token={apiKey}".isValidTileUrlTemplate(requireHttps = false), - ) + fun `unsafe and unresolved templates are rejected even where plain http is allowed`() { + assertFalse("http://token@tiles.example.org/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("http:///tiles/{z}/{x}/{y}.png".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("http://tiles.example.org/static#{z}/{x}/{y}".isValidTileUrlTemplate(cleartextAllowed)) + assertFalse("http://tiles.example.org/{z}/{x}/{y}.png?token={apiKey}".isValidTileUrlTemplate(cleartextAllowed)) } } diff --git a/core/repository/src/iosMain/kotlin/org/meshtastic/core/repository/Location.kt b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.ios.kt similarity index 87% rename from core/repository/src/iosMain/kotlin/org/meshtastic/core/repository/Location.kt rename to feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.ios.kt index e7abe31bb6..a0fd985a4a 100644 --- a/core/repository/src/iosMain/kotlin/org/meshtastic/core/repository/Location.kt +++ b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.ios.kt @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.repository +package org.meshtastic.feature.map.tiles -/** No-op stub for Location on iOS. */ -actual class Location +actual fun isCleartextPermitted(host: String): Boolean = true diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.android.kt b/feature/map/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvmAndroid.kt similarity index 89% rename from feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.android.kt rename to feature/map/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvmAndroid.kt index 1fcc439083..feee2ec15c 100644 --- a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.android.kt +++ b/feature/map/src/jvmAndroidMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvmAndroid.kt @@ -16,11 +16,6 @@ */ package org.meshtastic.feature.map.kml -// Duplicated verbatim in androidMain and jvmMain rather than shared from a custom `jvmAndroid` source-set group: -// the hierarchy-template group would not attach to the AGP-owned android target, and a hand-written dependsOn edge -// disables the default template and silently drops iosMain. A few dozen lines twice is cheaper than either failure -// mode. - import co.touchlab.kermit.Logger import org.meshtastic.feature.map.layers.MAX_KMZ_INFLATED_BYTES import org.meshtastic.feature.map.layers.isKmzArchive diff --git a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvm.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvm.kt deleted file mode 100644 index 1fcc439083..0000000000 --- a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/kml/KmlDocument.jvm.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.map.kml - -// Duplicated verbatim in androidMain and jvmMain rather than shared from a custom `jvmAndroid` source-set group: -// the hierarchy-template group would not attach to the AGP-owned android target, and a hand-written dependsOn edge -// disables the default template and silently drops iosMain. A few dozen lines twice is cheaper than either failure -// mode. - -import co.touchlab.kermit.Logger -import org.meshtastic.feature.map.layers.MAX_KMZ_INFLATED_BYTES -import org.meshtastic.feature.map.layers.isKmzArchive -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.util.zip.ZipInputStream - -actual fun readKmlDocument(bytes: ByteArray): String? = if (bytes.isKmzArchive()) { - ZipInputStream(ByteArrayInputStream(bytes)).use { zip -> - generateSequence { zip.nextEntry } - .firstOrNull { !it.isDirectory && it.name.endsWith(".kml", ignoreCase = true) } - ?.let { zip.readEntryWithin(MAX_KMZ_INFLATED_BYTES)?.decodeToString() } - } -} else { - bytes.decodeToString() -} - -actual fun readKmlArchiveImages(bytes: ByteArray, hrefs: Set): Map { - if (hrefs.isEmpty() || !bytes.isKmzArchive()) return emptyMap() - val images = mutableMapOf() - var budget = MAX_KMZ_INFLATED_BYTES - ZipInputStream(ByteArrayInputStream(bytes)).use { zip -> - var entry = zip.nextEntry - while (entry != null) { - if (!entry.isDirectory && entry.name in hrefs) { - // Budget blown mid-archive: stop reading but keep the images already extracted — each draws on its - // own — and let the overlays whose files went unread be skipped by the caller as "not packed". - val data = zip.readEntryWithin(budget) ?: break - budget -= data.size - images[entry.name] = data - } - entry = zip.nextEntry - } - } - return images -} - -/** - * This entry inflated, or null once it would take the archive past [remaining] bytes. - * - * The inflated size cannot be trusted from the entry header (a hostile zip lies there), so the cap is enforced on the - * bytes actually produced. `InputStream.readNBytes` would do this but is API 33+ on Android; minSdk is 26. - */ -private fun ZipInputStream.readEntryWithin(remaining: Long): ByteArray? { - val out = ByteArrayOutputStream() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - while (true) { - val read = read(buffer) - if (read == -1) return out.toByteArray() - if (out.size() + read > remaining) { - Logger.withTag("KmlDocument").w { "Refusing a KMZ entry past the ${MAX_KMZ_INFLATED_BYTES}B inflate cap" } - return null - } - out.write(buffer, 0, read) - } -} diff --git a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/layers/MapLayerPicker.jvm.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/layers/MapLayerPicker.jvm.kt index 9f410e3176..4797e0c312 100644 --- a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/layers/MapLayerPicker.jvm.kt +++ b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/layers/MapLayerPicker.jvm.kt @@ -19,9 +19,9 @@ package org.meshtastic.feature.map.layers import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.meshtastic.core.common.util.ioDispatcher import java.awt.FileDialog import java.awt.Frame import java.io.File @@ -32,7 +32,7 @@ actual fun rememberMapLayerPicker(onPick: (PickedMapFile) -> Unit): MapLayerPick return MapLayerPickerLauncher { scope.launch { val chosen = - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { @Suppress("TooGenericExceptionCaught") try { // AWT's dialog rather than Swing's chooser: it is the platform's own, which matters for a @@ -55,7 +55,7 @@ actual fun rememberMapLayerPicker(onPick: (PickedMapFile) -> Unit): MapLayerPick displayName = chosen.name, extensionOrMime = chosen.extension.ifBlank { null }, read = { - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { @Suppress("TooGenericExceptionCaught") try { chosen.readBytes() diff --git a/core/repository/src/jvmMain/kotlin/org/meshtastic/core/repository/Location.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.jvm.kt similarity index 84% rename from core/repository/src/jvmMain/kotlin/org/meshtastic/core/repository/Location.kt rename to feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.jvm.kt index 58ee000e20..a0fd985a4a 100644 --- a/core/repository/src/jvmMain/kotlin/org/meshtastic/core/repository/Location.kt +++ b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/tiles/CleartextPolicy.jvm.kt @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.repository +package org.meshtastic.feature.map.tiles -/** JVM placeholder location type for repository smoke compilation. */ -actual class Location +actual fun isCleartextPermitted(host: String): Boolean = true diff --git a/feature/messaging/README.md b/feature/messaging/README.md index 4bfb53cc97..802c190abb 100644 --- a/feature/messaging/README.md +++ b/feature/messaging/README.md @@ -28,14 +28,10 @@ Uses `HomoglyphCharacterStringTransformer` (from `:core:common`) to optionally r graph TB :feature:messaging[messaging]:::kmp-feature :feature:messaging -.-> :core:common - :feature:messaging -.-> :core:data - :feature:messaging -.-> :core:database - :feature:messaging -.-> :core:domain :feature:messaging -.-> :core:model :feature:messaging -.-> :core:navigation - :feature:messaging -.-> :core:prefs + :feature:messaging -.-> :core:repository :feature:messaging -.-> :core:resources - :feature:messaging -.-> :core:service :feature:messaging -.-> :core:ui :feature:messaging -.-> :core:testing diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 4a5c66230b..1cdfe8b2f1 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -18,18 +18,15 @@ plugins { alias(libs.plugins.meshtastic.kmp.feature) } kotlin { + // No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT. sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.navigation) - implementation(projects.core.prefs) implementation(libs.meshtastic.protobufs) + implementation(projects.core.repository) implementation(projects.core.resources) - implementation(projects.core.service) implementation(projects.core.ui) implementation(libs.androidx.paging.common) diff --git a/feature/messaging/detekt-baseline.xml b/feature/messaging/detekt-baseline.xml index 520ad131b5..23bd7c18a7 100644 --- a/feature/messaging/detekt-baseline.xml +++ b/feature/messaging/detekt-baseline.xml @@ -2,6 +2,9 @@ + AbstractClassCanBeInterface:MessageScreenComponents.kt:MessageMenuAction$MessageMenuAction + AbstractClassCanBeInterface:MessageTranslationService.kt:DownloadResult$DownloadResult + AbstractClassCanBeInterface:MessageTranslationService.kt:TranslationResult$TranslationResult ComposableParamOrder:MessageActions.kt:@Composable internal fun MessageActions ComposableParamOrder:MessageItem.kt:@OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun MessageItem ComposableParamOrder:MessageListPaged.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun MessageListPagedContent @@ -13,6 +16,7 @@ LambdaParameterEventTrailing:Message.kt:onSendMessage: () -> Unit LambdaParameterEventTrailing:MessageScreenComponents.kt:onClick: (QuickChatAction) -> Unit LambdaParameterEventTrailing:QuickChat.kt:onNavigateUp: () -> Unit + LongParameterList:Contacts.kt:private fun LazyListScope.contactSection ModifierMissing:Contacts.kt:@Suppress("LongMethod", "CyclomaticComplexMethod", "LongParameterList") @Composable fun ContactsScreen ModifierMissing:DeliveryInfoDialog.kt:@Composable fun DeliveryInfo ModifierMissing:Message.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalFoundationApi::class) @Composable fun MessageScreen @@ -23,6 +27,8 @@ ModifierMissing:MessageScreenComponents.kt:@OptIn(ExperimentalMaterial3Api::class) @Composable fun MessageTopBar ModifierMissing:Share.kt:@Composable fun ShareScreen ModifierNotUsedAtRoot:QuickChat.kt:modifier = modifier.fillMaxSize().padding(innerPadding) + NoNameShadowing:Message.kt:message + NoNameShadowing:Reaction.kt:reactions ParameterNaming:Contacts.kt:onDeleteSelected: () -> Unit ParameterNaming:Contacts.kt:onMuteSelected: () -> Unit ParameterNaming:MessageScreenComponents.kt:onToggleFilteringDisabled: () -> Unit @@ -35,5 +41,12 @@ PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview + UnnecessaryLaunchedEffect:Message.kt:LaunchedEffect + UnnecessaryLaunchedEffect:MessageListPaged.kt:LaunchedEffect + UnnecessaryLaunchedEffect:QuickChat.kt:LaunchedEffect + UnnecessaryLaunchedEffect:Share.kt:LaunchedEffect + UnusedPrivateProperty:MessageViewModel.kt:MessageViewModel$private val connectionStateProvider: ConnectionStateProvider + UnusedPrivateProperty:MessageViewModel.kt:MessageViewModel$private val homoglyphEncodingPrefs: HomoglyphPrefs + UseOrEmpty:Reaction.kt:groupedEmojis[it] ?: emptyList() diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt index e79486f851..750d9f2e41 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -91,12 +91,12 @@ import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer -import org.meshtastic.core.database.entity.QuickChatAction import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.ContactKey import org.meshtastic.core.model.MENTION_TOKEN_REGEX import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.archived_channel_read_only @@ -104,7 +104,7 @@ import org.meshtastic.core.resources.send import org.meshtastic.core.resources.type_a_message import org.meshtastic.core.resources.unknown_channel import org.meshtastic.core.ui.component.InlineStyle -import org.meshtastic.core.ui.component.SharedContactDialog +import org.meshtastic.core.ui.component.ShareContactDialog import org.meshtastic.core.ui.component.smartScrollToIndex import org.meshtastic.core.ui.icon.History import org.meshtastic.core.ui.icon.MeshtasticIcons @@ -187,6 +187,7 @@ fun MessageScreen( val filteredCount by viewModel.filteredCount.collectAsStateWithLifecycle() val showFiltered by viewModel.showFiltered.collectAsStateWithLifecycle() val filteringDisabled = contactSettings[contactKey]?.filteringDisabled ?: false + val messageFilterEnabled by viewModel.messageFilterEnabled.collectAsStateWithLifecycle() val isSearchActive by viewModel.isSearchActive.collectAsStateWithLifecycle() val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() val searchResults by viewModel.searchResults.collectAsStateWithLifecycle() @@ -394,7 +395,7 @@ fun MessageScreen( onDismiss = viewModel::dismissTranslationDialog, ) - sharedContact?.let { contact -> SharedContactDialog(contact = contact, onDismiss = { sharedContact = null }) } + sharedContact?.let { contact -> ShareContactDialog(contact = contact, onDismiss = { sharedContact = null }) } val originalMessage by remember(replyingToPacketId, pagedMessages.itemCount) { @@ -462,6 +463,7 @@ fun MessageScreen( showQuickChat = showQuickChat, onToggleQuickChat = viewModel::toggleShowQuickChat, onNavigateToQuickChatOptions = navigateToQuickChatOptions, + showFilterToggle = messageFilterEnabled, filteringDisabled = filteringDisabled, onToggleFilteringDisabled = { viewModel.setContactFilteringDisabled(contactKey, !filteringDisabled) @@ -552,7 +554,9 @@ fun MessageScreen( }, onClickChip = { onEvent(MessageScreenEvent.NodeDetails(it)) }, onDeleteMessages = { viewModel.deleteMessages(it) }, - onSendMessage = { text, key -> if (!isRetiredChannel) viewModel.sendMessage(text, key) }, + onResendMessage = { message -> + viewModel.resendMessage(message.uuid, message.text, contactKey) + }, onReply = { message -> if (!isRetiredChannel) replyingToPacketId = message?.packetId }, onTranslate = { onEvent(MessageScreenEvent.TranslateMessage(it)) }, onToggleTranslation = { onEvent(MessageScreenEvent.ToggleShowTranslated(it)) }, @@ -663,16 +667,19 @@ internal fun liveInlineMarkdownStyleRanges(source: String): List codeMatches.any { codeMatch -> match.range.first in codeMatch.range || match.range.last in codeMatch.range } } return buildList { - LIVE_BOLD.findAll(source).filterNot(isBlockedByCodeSpan).forEach { - add(LiveStyleSpan(it.groups[1]!!.range, InlineStyle.Bold)) - } - LIVE_ITALIC.findAll(source).filterNot(isBlockedByCodeSpan).forEach { - add(LiveStyleSpan(it.groups[1]!!.range, InlineStyle.Italic)) - } - LIVE_STRIKE.findAll(source).filterNot(isBlockedByCodeSpan).forEach { - add(LiveStyleSpan(it.groups[1]!!.range, InlineStyle.Strikethrough)) - } - codeMatches.forEach { add(LiveStyleSpan(it.groups[1]!!.range, InlineStyle.Code)) } + LIVE_BOLD.findAll(source) + .filterNot(isBlockedByCodeSpan) + .mapNotNull { it.groups[1] } + .forEach { add(LiveStyleSpan(it.range, InlineStyle.Bold)) } + LIVE_ITALIC.findAll(source) + .filterNot(isBlockedByCodeSpan) + .mapNotNull { it.groups[1] } + .forEach { add(LiveStyleSpan(it.range, InlineStyle.Italic)) } + LIVE_STRIKE.findAll(source) + .filterNot(isBlockedByCodeSpan) + .mapNotNull { it.groups[1] } + .forEach { add(LiveStyleSpan(it.range, InlineStyle.Strikethrough)) } + codeMatches.mapNotNull { it.groups[1] }.forEach { add(LiveStyleSpan(it.range, InlineStyle.Code)) } } .sortedWith(compareBy({ it.range.first }, { it.range.last }, { it.style.ordinal })) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index 18d5bc2d40..a191e739f9 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -64,6 +64,7 @@ import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.Reaction +import org.meshtastic.core.ui.component.ListScrollbar import org.meshtastic.feature.messaging.component.DateSeparator import org.meshtastic.feature.messaging.component.MessageItem import org.meshtastic.feature.messaging.component.MessageStatusDialog @@ -90,7 +91,8 @@ internal data class MessageListHandlers( val onSendReaction: (String, Int) -> Unit, val onClickChip: (Node) -> Unit, val onDeleteMessages: (List) -> Unit, - val onSendMessage: (String, String) -> Unit, + /** Replaces a failed message with a fresh send of its text; the action decides whether sending is allowed. */ + val onResendMessage: (Message) -> Unit, val onReply: (Message?) -> Unit, val onTranslate: (Message) -> Unit = {}, val onToggleTranslation: (Message) -> Unit = {}, @@ -148,12 +150,7 @@ internal fun MessageListPaged( isDirectMessage = isDirectMessageConversation, resendOption = message.isStatusRetryable(isDirectMessageConversation) && state.canSend, onResend = { - // Resend deletes the old row and sends a fresh one. Never take the first half without the second: - // on an archived conversation the send is refused, which would leave the message simply gone. - if (state.canSend) { - handlers.onDeleteMessages(listOf(message.uuid)) - handlers.onSendMessage(message.text, state.contactKey) - } + handlers.onResendMessage(message) showStatusDialog = null }, onDismiss = { showStatusDialog = null }, @@ -356,6 +353,7 @@ private fun MessageListPagedContent( } } } + ListScrollbar(listState) } } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index d1f030f616..287ea7b60e 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -29,10 +29,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -43,6 +41,7 @@ import kotlinx.coroutines.withContext import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.currentLocaleCode import org.meshtastic.core.common.util.ioDispatcher +import org.meshtastic.core.model.ContactKey import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node @@ -50,6 +49,7 @@ import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.repository.ActiveConversationTracker import org.meshtastic.core.repository.ConnectionStateProvider import org.meshtastic.core.repository.CustomEmojiPrefs +import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.MeshNotificationManager import org.meshtastic.core.repository.MessagingController @@ -105,6 +105,7 @@ class MessageViewModel( private val uiPrefs: UiPrefs, private val customEmojiPrefs: CustomEmojiPrefs, private val homoglyphEncodingPrefs: HomoglyphPrefs, + filterPrefs: FilterPrefs, private val meshNotificationManager: MeshNotificationManager, private val activeConversationTracker: ActiveConversationTracker, private val sendMessageUseCase: SendMessageUseCase, @@ -163,12 +164,11 @@ class MessageViewModel( _draftMessage.value = text val contactKey = draftContactKey ?: return pendingDraftPersistence?.cancel() - pendingDraftPersistence = - viewModelScope.launch { - delay(DRAFT_PERSISTENCE_DELAY_MS) - savedStateHandle[draftKey(contactKey)] = text - withContext(ioDispatcher) { packetRepository.setDraft(contactKey, text) } - } + pendingDraftPersistence = viewModelScope.launch { + delay(DRAFT_PERSISTENCE_DELAY_MS) + savedStateHandle[draftKey(contactKey)] = text + withContext(ioDispatcher) { packetRepository.setDraft(contactKey, text) } + } } fun clearDraftMessage() { @@ -193,6 +193,8 @@ class MessageViewModel( val showFullMessageTimestamps = uiPrefs.showFullMessageTimestamps + val messageFilterEnabled = filterPrefs.filterEnabled + private val _showFiltered = MutableStateFlow(false) val showFiltered: StateFlow = _showFiltered.asStateFlow() @@ -357,11 +359,11 @@ class MessageViewModel( if (contactKeyForPagedMessages.value != contactKey) { contactKeyForPagedMessages.value = contactKey } - return flow { emitAll(packetRepository.getMessagesFrom(contactKey, limit = limit, getNode = ::getNode)) } + return packetRepository.getMessagesFrom(contactKey, limit = limit, getNode = ::getNode) } fun toggleShowQuickChat() { - uiPrefs.setShowQuickChat(!uiPrefs.showQuickChat.value) + uiPrefs.toggleShowQuickChat() } fun toggleShowFiltered() { @@ -403,12 +405,25 @@ class MessageViewModel( fun deleteMessages(uuidList: List) = safeLaunch(context = ioDispatcher, tag = "deleteMessages") { packetRepository.deleteMessages(uuidList) } + /** + * Replaces message [uuid] with a fresh send of [text]. The original row is deleted only after the new one is + * queued, so a refused or failed send leaves it in place rather than losing the message. + */ + fun resendMessage(uuid: Long, text: String, contactKey: String) { + // A retired conversation has no channel to send on; refuse here, where the delete would otherwise follow. + if (ContactKey(contactKey).isRetired) return + safeLaunch(errorEvents = sendErrorEvents, tag = "resendMessage") { + packetRepository.replaceMessage(uuid) { sendMessageUseCase.invoke(text, contactKey, null) } + } + } + // region ── Translation ── /** Whether on-device translation into the current locale is possible (always false on F-Droid/desktop). */ - val translationAvailable: StateFlow = - flow { emit(messageTranslationService.isLanguageAvailable(currentLocaleCode())) } - .stateInWhileSubscribed(initialValue = false) + val translationAvailable: StateFlow = flow { + emit(messageTranslationService.isLanguageAvailable(currentLocaleCode())) + } + .stateInWhileSubscribed(initialValue = false) private val _translationDialogState = MutableStateFlow(TranslationDialogState.Hidden) val translationDialogState: StateFlow = _translationDialogState.asStateFlow() diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt index 662b6ce118..019f917681 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt @@ -57,7 +57,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add import org.meshtastic.core.resources.cancel diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt index 56776f1438..465e3e0d8a 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt @@ -18,7 +18,7 @@ package org.meshtastic.feature.messaging import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.ui.theme.AppTheme @PreviewLightDark diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt index cf8c9ec3bd..417fcf03a2 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt @@ -19,7 +19,7 @@ package org.meshtastic.feature.messaging import androidx.lifecycle.ViewModel import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.ioDispatcher -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.repository.QuickChatActionRepository import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt index da7b9ead4a..c92ee3cf44 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,27 +72,37 @@ private fun ReplyButton(onClick: () -> Unit = {}) = IconButton( ) @Composable -internal fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) = - AnimatedVisibility(visible = fromLocal) { - IconButton(onClick = onStatusClick) { - Crossfade(targetState = status, label = "MessageStatusIcon") { currentStatus -> - Icon( - imageVector = - when (currentStatus) { - MessageStatus.RECEIVED -> MeshtasticIcons.Acknowledged - MessageStatus.QUEUED -> MeshtasticIcons.CloudUpload - MessageStatus.DELIVERED -> MeshtasticIcons.MqttDelivered - MessageStatus.SFPP_ROUTING -> MeshtasticIcons.AddLink - MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.LinkIcon - MessageStatus.ENROUTE -> MeshtasticIcons.MessageEnroute - MessageStatus.ERROR -> MeshtasticIcons.MessageError - else -> MeshtasticIcons.Warning - }, - contentDescription = stringResource(Res.string.message_delivery_status), - ) - } +internal fun MessageStatusButton( + status: MessageStatus, + fromLocal: Boolean, + onStatusClick: () -> Unit = {}, + isWarning: Boolean = false, +) = AnimatedVisibility(visible = fromLocal) { + IconButton(onClick = onStatusClick) { + Crossfade(targetState = status, label = "MessageStatusIcon") { currentStatus -> + Icon( + imageVector = + when (currentStatus) { + MessageStatus.RECEIVED -> MeshtasticIcons.Acknowledged + MessageStatus.QUEUED -> MeshtasticIcons.CloudUpload + MessageStatus.DELIVERED -> MeshtasticIcons.MqttDelivered + MessageStatus.SFPP_ROUTING -> MeshtasticIcons.AddLink + MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.LinkIcon + MessageStatus.ENROUTE -> MeshtasticIcons.MessageEnroute + MessageStatus.ERROR -> MeshtasticIcons.MessageError + else -> MeshtasticIcons.Warning + }, + contentDescription = stringResource(Res.string.message_delivery_status), + tint = + if (isWarning) { + messageStatusColor(currentStatus, isWarning = true) + } else { + LocalContentColor.current + }, + ) } } +} @Composable internal fun MessageActions( diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt index f7a53bdaf1..3301c6d7d9 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.getAckProofStatusRes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.action_copy_message import org.meshtastic.core.resources.action_delete_message @@ -69,14 +70,17 @@ import org.meshtastic.core.resources.timestamp import org.meshtastic.core.resources.translate import org.meshtastic.core.ui.icon.AddReaction import org.meshtastic.core.ui.icon.Copy +import org.meshtastic.core.ui.icon.Dangerous import org.meshtastic.core.ui.icon.Delete import org.meshtastic.core.ui.icon.History +import org.meshtastic.core.ui.icon.KeyOff import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.More import org.meshtastic.core.ui.icon.Reply import org.meshtastic.core.ui.icon.SelectAll import org.meshtastic.core.ui.icon.ShieldCheck import org.meshtastic.core.ui.icon.Translate +import org.meshtastic.proto.MeshPacket @Suppress("LongMethod") @Composable @@ -95,6 +99,7 @@ fun MessageActionsContent( status: MessageStatus? = null, timestamp: String? = null, xeddsaSigned: Boolean = false, + ackProofStatus: Int = 0, translationRowState: TranslationRowState? = null, onTranslate: () -> Unit = {}, onToggleTranslation: () -> Unit = {}, @@ -122,6 +127,8 @@ fun MessageActionsContent( ) } + AckProofListItem(ackProofStatus) + // The caller supplies the same compact or full timestamp shown in the conversation header. if (timestamp != null) { ListItem( @@ -225,6 +232,33 @@ fun MessageActionsContent( } } +/** + * The radio's verdict on the ack that closed out this message. Silent when no proof was carried, which is every ack + * from firmware predating [MeshPacket.AckProofStatus] and every ack that simply did not carry one. + */ +@Composable +private fun AckProofListItem(ackProofStatus: Int) { + val (headline, supporting) = getAckProofStatusRes(ackProofStatus) ?: return + val proofStatus = MeshPacket.AckProofStatus.fromValue(ackProofStatus) + val icon = + when (proofStatus) { + MeshPacket.AckProofStatus.ACK_PROOF_INVALID -> MeshtasticIcons.Dangerous + MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY -> MeshtasticIcons.KeyOff + else -> MeshtasticIcons.ShieldCheck + } + val tint = + when (proofStatus) { + MeshPacket.AckProofStatus.ACK_PROOF_INVALID -> MaterialTheme.colorScheme.error + MeshPacket.AckProofStatus.ACK_PROOF_NO_KEY -> MaterialTheme.colorScheme.onSurfaceVariant + else -> MaterialTheme.colorScheme.primary + } + ListItem( + headlineContent = { Text(stringResource(headline)) }, + supportingContent = { Text(stringResource(supporting)) }, + leadingContent = { Icon(icon, contentDescription = null, tint = tint) }, + ) +} + internal const val MAX_EMOJI_ROW_SIZE = 6 /** diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index aa238d1593..a871e12914 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -80,6 +80,7 @@ import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction +import org.meshtastic.core.model.isAckProofForged import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.a11y_message_from import org.meshtastic.core.resources.action_show_message_status @@ -191,6 +192,7 @@ fun MessageItem( val statusString = message.getStatusStringRes(isDirectMessage) val isDirectImplicitAck = message.status == MessageStatus.DELIVERED && isDirectMessage val isRetryableFailure = message.status == MessageStatus.ERROR && message.isStatusRetryable(isDirectMessage) + val isForgedAck = isAckProofForged(message.ackProofStatus) // While searching, always show the original text — FTS matches and highlights apply to it, not the translation. val showsTranslation = message.showTranslated && message.translatedText != null && searchQuery.isEmpty() val bodyText = message.displayedText(searching = searchQuery.isNotEmpty()) @@ -235,6 +237,7 @@ fun MessageItem( // pulled the packet off the node, which is misleading after an offline backlog sync. timestamp = timestamp, xeddsaSigned = message.xeddsaSigned, + ackProofStatus = message.ackProofStatus, onStatus = onStatusClick, translationRowState = translationRowStateFor(message, translationAvailable), onTranslate = { @@ -539,7 +542,7 @@ fun MessageItem( status = message.status ?: MessageStatus.UNKNOWN, text = stringResource(statusString.second), metadataStyle = metadataStyle, - isWarning = isDirectImplicitAck || isRetryableFailure, + isWarning = isDirectImplicitAck || isRetryableFailure || isForgedAck, onStatusClick = onStatusClick, ) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItemPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItemPreviews.kt index 535fdbe097..7844c688ad 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItemPreviews.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItemPreviews.kt @@ -35,6 +35,7 @@ import org.meshtastic.core.resources.sample_message import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.feature.messaging.isSameGroup +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Routing @Suppress("PreviewPublic") @@ -87,6 +88,66 @@ fun MessageItemSignedPreview() { } } +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun MessageItemAckProofPreview() { + val ourNode = NodePreviewParameterProvider().mickeyMouse + val proven = + Message( + text = "Proven delivery - the addressed node signed for it.", + time = "14:10", + fromLocal = true, + status = MessageStatus.RECEIVED, + snr = null, + rssi = null, + hopsAway = 0, + uuid = 30L, + receivedTime = nowMillis, + node = ourNode, + read = true, + routingError = 0, + packetId = 7001, + emojis = listOf(), + replyId = null, + viaMqtt = false, + ackProofStatus = MeshPacket.AckProofStatus.ACK_PROOF_VALID.value, + ) + val forged = + proven.copy( + text = "Somebody acked this without the key.", + time = "14:11", + uuid = 31L, + packetId = 7002, + ackProofStatus = MeshPacket.AckProofStatus.ACK_PROOF_INVALID.value, + ) + val unproven = proven.copy(text = "No proof carried.", time = "14:12", uuid = 32L, ackProofStatus = 0) + AppTheme { + Column( + modifier = + Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(vertical = 16.dp), + ) { + listOf(proven, forged, unproven).forEach { msg -> + MessageItem( + message = msg, + node = msg.node, + selected = false, + ourNode = ourNode, + isDirectMessage = true, + onReply = {}, + sendReaction = {}, + onShowReactions = {}, + onClick = {}, + onLongClick = {}, + onDoubleClick = {}, + onClickChip = {}, + onNavigateToOriginalMessage = {}, + ) + } + } + } +} + @Suppress("PreviewPublic") @PreviewLightDark @Composable diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt index 296996cb11..bda1315bfa 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt @@ -69,10 +69,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.QuickChatAction import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.alert_bell_text import org.meshtastic.core.resources.cancel @@ -394,6 +394,7 @@ fun MessageTopBar( showQuickChat: Boolean, onToggleQuickChat: () -> Unit, onNavigateToQuickChatOptions: () -> Unit = {}, + showFilterToggle: Boolean = true, filteringDisabled: Boolean = false, onToggleFilteringDisabled: () -> Unit = {}, filteredCount: Int = 0, @@ -433,6 +434,7 @@ fun MessageTopBar( onNavigateToQuickChatOptions = onNavigateToQuickChatOptions, channelIndex = channelIndex, mismatchKey = mismatchKey, + showFilterToggle = showFilterToggle, filteringDisabled = filteringDisabled, onToggleFilteringDisabled = onToggleFilteringDisabled, filteredCount = filteredCount, @@ -450,6 +452,7 @@ private fun MessageTopBarActions( onNavigateToQuickChatOptions: () -> Unit, channelIndex: Int?, mismatchKey: Boolean, + showFilterToggle: Boolean, filteringDisabled: Boolean, onToggleFilteringDisabled: () -> Unit, filteredCount: Int, @@ -471,6 +474,7 @@ private fun MessageTopBarActions( showQuickChat = showQuickChat, onToggleQuickChat = onToggleQuickChat, onNavigateToQuickChatOptions = onNavigateToQuickChatOptions, + showFilterToggle = showFilterToggle, filteringDisabled = filteringDisabled, onToggleFilteringDisabled = onToggleFilteringDisabled, filteredCount = filteredCount, @@ -488,6 +492,7 @@ private fun OverFlowMenu( showQuickChat: Boolean, onToggleQuickChat: () -> Unit, onNavigateToQuickChatOptions: () -> Unit, + showFilterToggle: Boolean, filteringDisabled: Boolean, onToggleFilteringDisabled: () -> Unit, filteredCount: Int, @@ -505,7 +510,9 @@ private fun OverFlowMenu( if (filteredCount > 0 && !filteringDisabled) { FilteredMessagesMenuItem(showFiltered, filteredCount, onDismiss, onToggleShowFiltered) } - FilterToggleMenuItem(filteringDisabled, onDismiss, onToggleFilteringDisabled) + if (showFilterToggle) { + FilterToggleMenuItem(filteringDisabled, onDismiss, onToggleFilteringDisabled) + } FilterSettingsMenuItem(onDismiss, onNavigateToFilterSettings) } } @@ -650,15 +657,14 @@ fun handleQuickChatAction( when (action.mode) { QuickChatAction.Mode.Append -> { if (!currentText.contains(action.message)) { - val newText = - buildString { - append(currentText) - if (currentText.isNotEmpty() && !currentText.endsWith(' ')) { - append(' ') - } - append(action.message) + val newText = buildString { + append(currentText) + if (currentText.isNotEmpty() && !currentText.endsWith(' ')) { + append(' ') } - .limitBytes(MESSAGE_CHARACTER_LIMIT_BYTES) + append(action.message) + } + .limitBytes(MESSAGE_CHARACTER_LIMIT_BYTES) onUpdateText(newText) } } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageStatusIcon.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageStatusIcon.kt index d3b3aedefb..6192575d1a 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageStatusIcon.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageStatusIcon.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse @@ -74,6 +75,7 @@ fun MessageStatusIcon( } @Composable +@ReadOnlyComposable internal fun messageStatusColor(status: MessageStatus, isWarning: Boolean = false): Color { val colorScheme = MaterialTheme.colorScheme if (isWarning) { diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index e2295721f1..872cc427db 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -61,10 +61,12 @@ import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.getMessageStatusDetailRes import org.meshtastic.core.model.getMessageStatusStringRes +import org.meshtastic.core.model.isAckProofForged import org.meshtastic.core.model.isMessageStatusRetryable import org.meshtastic.core.model.util.getShortDateTime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.react +import org.meshtastic.core.resources.security_signed_verified import org.meshtastic.core.resources.you import org.meshtastic.core.ui.component.Rssi import org.meshtastic.core.ui.component.Snr @@ -72,6 +74,9 @@ import org.meshtastic.core.ui.emoji.EmojiPickerDialog import org.meshtastic.core.ui.icon.AddReaction import org.meshtastic.core.ui.icon.HopCount import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.ShieldCheck +import org.meshtastic.core.ui.theme.StatusColors.StatusGreen +import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.feature.messaging.DeliveryInfo @Composable @@ -80,6 +85,7 @@ internal fun ReactionItem( emoji: String, emojiCount: Int = 1, status: MessageStatus = MessageStatus.UNKNOWN, + isWarning: Boolean = false, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, ) { @@ -104,10 +110,10 @@ internal fun ReactionItem( BorderStroke( width = 1.dp, color = - if (isError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) + when { + isError -> MaterialTheme.colorScheme.error + isWarning -> MaterialTheme.colorScheme.StatusYellow + else -> MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) }, ), ) { @@ -152,6 +158,7 @@ internal fun ReactionRow( emoji = emoji, emojiCount = reactions.size, status = localReaction?.status ?: MessageStatus.RECEIVED, + isWarning = localReaction != null && isAckProofForged(localReaction.ackProofStatus), onClick = { if (canReact) onSendReaction(emoji) }, onLongClick = onShowReactions, ) @@ -216,7 +223,13 @@ internal fun ReactionDialog( var showStatusDialog by remember { mutableStateOf(null) } showStatusDialog?.let { reaction -> val isDirectMessage = NodeAddress.fromString(reaction.to) !is NodeAddress.Broadcast - val (title, text) = getMessageStatusStringRes(reaction.status, reaction.routingError, isDirectMessage) + val (title, text) = + getMessageStatusStringRes( + reaction.status, + reaction.routingError, + isDirectMessage, + reaction.ackProofStatus, + ) DeliveryInfo( title = title, @@ -276,12 +289,32 @@ internal fun ReactionDialog( } else { reaction.user.long_name } - Text(text = displayName, style = MaterialTheme.typography.titleMedium) + Row( + modifier = Modifier.weight(1f, fill = false), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = displayName, + modifier = Modifier.weight(1f, fill = false), + style = MaterialTheme.typography.titleMedium, + ) + // Set only on verified broadcasts, so a DM reaction never shows it. + if (!isLocal && reaction.xeddsaSigned) { + Icon( + imageVector = MeshtasticIcons.ShieldCheck, + contentDescription = stringResource(Res.string.security_signed_verified), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.StatusGreen, + ) + } + } Row(verticalAlignment = Alignment.CenterVertically) { if (isLocal) { MessageStatusButton( status = reaction.status, fromLocal = true, + isWarning = isAckProofForged(reaction.ackProofStatus), onStatusClick = { showStatusDialog = reaction }, ) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt index 5633d5f702..52aea71b81 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt @@ -19,6 +19,7 @@ package org.meshtastic.feature.messaging.navigation import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavBackStack @@ -94,6 +95,7 @@ fun EntryProviderScope.contactsGraph( } @Composable +@NonRestartableComposable fun ContactsEntryContent( backStack: NavBackStack, scrollToTopEvents: Flow, diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt index f485268653..ef027df632 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt @@ -25,6 +25,7 @@ import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.navigation.ChannelsRoute import org.meshtastic.core.navigation.ContactsRoute import org.meshtastic.core.navigation.NodesRoute +import org.meshtastic.core.navigation.SettingsRoute import org.meshtastic.core.ui.component.ScrollToTopEvent @Composable @@ -42,6 +43,7 @@ fun AdaptiveContactsScreen( onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, + onNavigateToFilterSettings = { backStack.add(SettingsRoute.FilterSettings) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, ) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt index d39130cc16..f3098c2ed9 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt @@ -100,6 +100,7 @@ import org.meshtastic.core.resources.delete_messages import org.meshtastic.core.resources.delete_selection import org.meshtastic.core.resources.direct_messages import org.meshtastic.core.resources.expanded +import org.meshtastic.core.resources.filter_settings import org.meshtastic.core.resources.mark_as_read import org.meshtastic.core.resources.mark_unread_selected import org.meshtastic.core.resources.mute_1_week @@ -131,6 +132,7 @@ import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.Delete import org.meshtastic.core.ui.icon.ExpandLess import org.meshtastic.core.ui.icon.ExpandMore +import org.meshtastic.core.ui.icon.FilterList import org.meshtastic.core.ui.icon.Keep import org.meshtastic.core.ui.icon.MarkChatRead import org.meshtastic.core.ui.icon.MarkChatUnread @@ -152,6 +154,7 @@ fun ContactsScreen( onClickNodeChip: (Int) -> Unit, onNavigateToMessages: (String) -> Unit, onNavigateToNodeDetails: (Int) -> Unit, + onNavigateToFilterSettings: () -> Unit, scrollToTopEvents: Flow?, activeContactKey: String?, ) { @@ -181,9 +184,9 @@ fun ContactsScreen( } } - // Derived state for selected contacts and count - val selectedContacts = - remember(contacts, selectedContactKeys) { contacts.filter { it.contactKey in selectedContactKeys } } + // selectedContactKeys is mutated in place, so as a remember key it never changes; read it as state instead. + val selectedContacts by + remember(contacts) { derivedStateOf { contacts.filter { it.contactKey in selectedContactKeys } } } // Get message count directly from repository for selected contacts var selectedCount by remember { mutableIntStateOf(0) } LaunchedEffect(selectedContactKeys.size, selectedContactKeys.joinToString(",")) { @@ -252,6 +255,12 @@ fun ContactsScreen( canNavigateUp = false, onNavigateUp = {}, actions = { + IconButton(onClick = onNavigateToFilterSettings) { + Icon( + MeshtasticIcons.FilterList, + contentDescription = stringResource(Res.string.filter_settings), + ) + } val unreadCountTotal by viewModel.unreadCountTotal.collectAsStateWithLifecycle(0) if (unreadCountTotal > 0) { IconButton(onClick = { viewModel.markAllAsRead() }) { diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index 6c4dad5c94..e28cdffef8 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -100,7 +100,7 @@ class ContactsViewModel( packetRepository.getContactSettings(), keyedNodeNums, ) { identity, contacts, channelSet, settings, keyedNodes -> - val (myNodeInfo, myId) = identity + val (myNodeInfo, _) = identity val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList() // Add empty channel placeholders (always show Broadcast contacts, even when empty) val placeholder = diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelResendTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelResendTest.kt new file mode 100644 index 0000000000..aae7178320 --- /dev/null +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelResendTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging + +import androidx.lifecycle.SavedStateHandle +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify.VerifyMode +import dev.mokkery.verifySuspend +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.repository.ActiveConversationTracker +import org.meshtastic.core.repository.ConnectionStateProvider +import org.meshtastic.core.repository.CustomEmojiPrefs +import org.meshtastic.core.repository.HomoglyphPrefs +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.MessagingController +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.QuickChatActionRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.repository.usecase.SendMessageOutcome +import org.meshtastic.core.repository.usecase.SendMessageUseCase +import org.meshtastic.core.testing.FakeFilterPrefs +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.ui.util.SnackbarManager +import org.meshtastic.feature.messaging.translation.MessageTranslationService +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.DeviceProfile +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.LocalModuleConfig +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class MessageViewModelResendTest { + + private val packetRepository: PacketRepository = mock(MockMode.autofill) + private val sendMessageUseCase: SendMessageUseCase = mock(MockMode.autofill) + private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) + private val quickChatActionRepository: QuickChatActionRepository = mock(MockMode.autofill) + private val connectionStateProvider: ConnectionStateProvider = mock(MockMode.autofill) + private val customEmojiPrefs: CustomEmojiPrefs = mock(MockMode.autofill) + private val homoglyphPrefs: HomoglyphPrefs = mock(MockMode.autofill) + private val uiPrefs: UiPrefs = mock(MockMode.autofill) + private val testDispatcher = StandardTestDispatcher() + private val calls = mutableListOf() + private lateinit var viewModel: MessageViewModel + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) + every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig.Builder().build()) + every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile.Builder().build()) + every { customEmojiPrefs.customEmojiFrequency } returns MutableStateFlow(null) + every { homoglyphPrefs.homoglyphEncodingEnabled } returns MutableStateFlow(false) + every { uiPrefs.showQuickChat } returns MutableStateFlow(false) + every { uiPrefs.showFullMessageTimestamps } returns MutableStateFlow(false) + every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) + every { packetRepository.getContactSettings() } returns MutableStateFlow(emptyMap()) + every { packetRepository.getFirstUnreadMessageUuid(any()) } returns MutableStateFlow(null) + every { packetRepository.hasUnreadMessages(any()) } returns MutableStateFlow(false) + every { packetRepository.getUnreadCountFlow(any()) } returns MutableStateFlow(0) + every { packetRepository.getFilteredCountFlow(any()) } returns MutableStateFlow(0) + every { quickChatActionRepository.getAllActions() } returns MutableStateFlow(emptyList()) + everySuspend { packetRepository.replaceMessage(any(), any()) } calls + { + calls += "replace:${it.args[0]}" + @Suppress("UNCHECKED_CAST") + val send = it.args[1] as suspend () -> Unit + send() + } + + viewModel = + MessageViewModel( + savedStateHandle = SavedStateHandle(mapOf("contactKey" to LIVE_CONTACT)), + nodeRepository = FakeNodeRepository(), + radioConfigRepository = radioConfigRepository, + quickChatActionRepository = quickChatActionRepository, + connectionStateProvider = connectionStateProvider, + messagingController = mock(MockMode.autofill), + packetRepository = packetRepository, + sendMessageUseCase = sendMessageUseCase, + customEmojiPrefs = customEmojiPrefs, + homoglyphEncodingPrefs = homoglyphPrefs, + filterPrefs = FakeFilterPrefs(), + uiPrefs = uiPrefs, + meshNotificationManager = mock(MockMode.autofill), + activeConversationTracker = ActiveConversationTracker(), + messageTranslationService = mock(MockMode.autofill), + snackbarManager = SnackbarManager(), + ) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `resend sends the new message inside the replacement of the original`() = runTest { + everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } calls + { + calls += "send" + SendMessageOutcome.Queued(1) + } + + viewModel.resendMessage(uuid = 42L, text = "Hello", contactKey = LIVE_CONTACT) + advanceUntilIdle() + + assertEquals(listOf("replace:42", "send"), calls) + verifySuspend { sendMessageUseCase.invoke("Hello", LIVE_CONTACT, null) } + } + + @Test + fun `resend into a retired conversation neither sends nor deletes`() = runTest { + val retired = ContactKey.retiredBroadcast("a1b2c3d4").value + + viewModel.resendMessage(uuid = 42L, text = "Hello", contactKey = retired) + advanceUntilIdle() + + verifySuspend(VerifyMode.not) { sendMessageUseCase.invoke(any(), any(), any()) } + verifySuspend(VerifyMode.not) { packetRepository.replaceMessage(any(), any()) } + } + + private companion object { + const val LIVE_CONTACT = "0!12345678" + } +} diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt index cd45f9e619..34ecbd1346 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt @@ -25,6 +25,7 @@ import dev.mokkery.every import dev.mokkery.everySuspend import dev.mokkery.matcher.any import dev.mokkery.mock +import dev.mokkery.verify import dev.mokkery.verify.VerifyMode import dev.mokkery.verifySuspend import kotlinx.coroutines.Dispatchers @@ -47,7 +48,9 @@ import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.QuickChatActionRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.repository.usecase.SendMessageOutcome import org.meshtastic.core.repository.usecase.SendMessageUseCase +import org.meshtastic.core.testing.FakeFilterPrefs import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.TestDataFactory import org.meshtastic.core.ui.util.SnackbarManager @@ -81,6 +84,7 @@ class MessageViewModelTest { private val customEmojiPrefs: CustomEmojiPrefs = mock(MockMode.autofill) private val homoglyphPrefs: HomoglyphPrefs = mock(MockMode.autofill) private val uiPrefs: UiPrefs = mock(MockMode.autofill) + private lateinit var filterPrefs: FakeFilterPrefs private val meshNotificationManager: org.meshtastic.core.repository.MeshNotificationManager = mock(MockMode.autofill) private val activeConversationTracker = ActiveConversationTracker() @@ -100,6 +104,7 @@ class MessageViewModelTest { Dispatchers.setMain(testDispatcher) savedStateHandle = SavedStateHandle(mapOf("contactKey" to "0!12345678")) nodeRepository = FakeNodeRepository() + filterPrefs = FakeFilterPrefs() connectionStateFlow.value = ConnectionState.Disconnected showQuickChatFlow.value = false @@ -118,7 +123,7 @@ class MessageViewModelTest { every { customEmojiPrefs.customEmojiFrequency } returns customEmojiFrequencyFlow every { homoglyphPrefs.homoglyphEncodingEnabled } returns MutableStateFlow(false) every { uiPrefs.showQuickChat } returns showQuickChatFlow - every { uiPrefs.setShowQuickChat(any()) } returns Unit + every { uiPrefs.toggleShowQuickChat() } returns Unit every { uiPrefs.showFullMessageTimestamps } returns showFullMessageTimestampsFlow every { packetRepository.getContactSettings() } returns contactSettingsFlow @@ -141,6 +146,7 @@ class MessageViewModelTest { sendMessageUseCase = sendMessageUseCase, customEmojiPrefs = customEmojiPrefs, homoglyphEncodingPrefs = homoglyphPrefs, + filterPrefs = filterPrefs, uiPrefs = uiPrefs, meshNotificationManager = meshNotificationManager, activeConversationTracker = activeConversationTracker, @@ -180,6 +186,13 @@ class MessageViewModelTest { @Test fun testInitialization() = runTest { assertNotNull(viewModel) } + @Test + fun testMessageFilterEnabledFollowsTheGlobalSetting() = runTest { + assertEquals(false, viewModel.messageFilterEnabled.value) + filterPrefs.setFilterEnabled(true) + assertEquals(true, viewModel.messageFilterEnabled.value) + } + private val draftContact = "0!12345678" /** Draft edits are ignored until the stored value has been read back, so every draft test loads first. */ @@ -272,17 +285,10 @@ class MessageViewModelTest { } @Test - fun testToggleShowQuickChat() = runTest { - viewModel.showQuickChat.test { - assertEquals(false, awaitItem()) + fun testToggleShowQuickChatDelegatesToThePrefsToggle() { + viewModel.toggleShowQuickChat() - viewModel.toggleShowQuickChat() - // Since setShowQuickChat is mocked to returns Unit, it doesn't update the flow. - // In a real app, the flow would update. We simulate it here. - showQuickChatFlow.value = true - assertEquals(true, awaitItem()) - cancelAndIgnoreRemainingEvents() - } + verify { uiPrefs.toggleShowQuickChat() } } @Test @@ -307,7 +313,7 @@ class MessageViewModelTest { @Test fun testSendMessage() = runTest { - everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns 1 + everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns SendMessageOutcome.Queued(1) viewModel.sendMessage("Hello", "0!12345678", null) diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt index 9c6bf3df8e..7048040215 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt @@ -53,6 +53,7 @@ import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.translation_failed import org.meshtastic.core.resources.translation_model_download_failed import org.meshtastic.core.resources.translation_not_required +import org.meshtastic.core.testing.FakeFilterPrefs import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.TestDataFactory import org.meshtastic.core.ui.util.SnackbarManager @@ -188,6 +189,7 @@ class MessageViewModelTranslationTest { sendMessageUseCase = sendMessageUseCase, customEmojiPrefs = customEmojiPrefs, homoglyphEncodingPrefs = homoglyphPrefs, + filterPrefs = FakeFilterPrefs(), uiPrefs = uiPrefs, meshNotificationManager = meshNotificationManager, activeConversationTracker = ActiveConversationTracker(), diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt index 38bb39b14e..b25176257e 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.model.QuickChatAction import org.meshtastic.core.repository.QuickChatActionRepository import kotlin.test.AfterTest import kotlin.test.BeforeTest diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsSelectionToolbarTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsSelectionToolbarTest.kt new file mode 100644 index 0000000000..bfbee531d3 --- /dev/null +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsSelectionToolbarTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging.ui.contact + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.lifecycle.SavedStateHandle +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.model.ContactSettings +import org.meshtastic.core.repository.ConnectionStateProvider +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.testing.TestDataFactory +import org.meshtastic.core.ui.util.SnackbarManager +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.ChannelSettings +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The toolbar derives pin and mute state from the conversations currently selected, so it has to follow the selection + * as rows are long-pressed rather than the list as it stood when the selection was last empty. + */ +@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class) +class ContactsSelectionToolbarTest { + + private val channelKey = ContactKey.broadcast(0).value + private val nodeRepository = FakeNodeRepository() + private val packetRepository: PacketRepository = mock(MockMode.autofill) + private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) + private val connectionStateProvider: ConnectionStateProvider = mock(MockMode.autofill) + private val pinWrites = MutableStateFlow(emptyList()) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo()) + every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) + every { packetRepository.getUnreadCountTotal() } returns MutableStateFlow(0) + every { packetRepository.getContacts() } returns MutableStateFlow(emptyMap()) + every { radioConfigRepository.channelSetFlow } returns + MutableStateFlow( + ChannelSet.Builder().settings(listOf(ChannelSettings.Builder().name(CHANNEL_NAME).build())).build(), + ) + everySuspend { packetRepository.setPinned(any(), true) } calls { pinWrites.update { it + true } } + everySuspend { packetRepository.setPinned(any(), false) } calls { pinWrites.update { it + false } } + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + private fun ComposeUiTest.showContacts(settings: ContactSettings) { + every { packetRepository.getContactSettings() } returns MutableStateFlow(mapOf(channelKey to settings)) + val viewModel = + ContactsViewModel( + savedStateHandle = SavedStateHandle(), + nodeRepository = nodeRepository, + packetRepository = packetRepository, + snackbarManager = SnackbarManager(), + radioConfigRepository = radioConfigRepository, + connectionStateProvider = connectionStateProvider, + ) + setContent { + MaterialTheme { + ContactsScreen( + onNavigateToShare = {}, + onHandleDeepLink = { _, _ -> }, + viewModel = viewModel, + onClickNodeChip = {}, + onNavigateToMessages = {}, + onNavigateToNodeDetails = {}, + onNavigateToFilterSettings = {}, + scrollToTopEvents = null, + activeContactKey = null, + ) + } + } + onNodeWithText(CHANNEL_NAME).performTouchInput { longClick() } + waitForIdle() + } + + @Test + fun selectingAPinnedConversationOffersUnpin() = runComposeUiTest { + showContacts(ContactSettings(contactKey = channelKey, pinned = true)) + + onNodeWithContentDescription("Unpin").performClick() + waitUntil { pinWrites.value.isNotEmpty() } + assertEquals(listOf(false), pinWrites.value) + } + + @Test + fun selectingAnUnmutedConversationOffersMute() = runComposeUiTest { + showContacts(ContactSettings(contactKey = channelKey)) + + onNodeWithContentDescription("Mute selected").assertExists() + onNodeWithContentDescription("Pin").assertExists() + } + + private companion object { + const val CHANNEL_NAME = "Ops" + } +} diff --git a/feature/node/README.md b/feature/node/README.md index 940e53a992..d2e83e490b 100644 --- a/feature/node/README.md +++ b/feature/node/README.md @@ -28,18 +28,13 @@ Provides a compass interface to show the relative direction and distance to othe graph TB :feature:node[node]:::kmp-feature :feature:node -.-> :core:common - :feature:node -.-> :core:data - :feature:node -.-> :core:database - :feature:node -.-> :core:datastore :feature:node -.-> :core:domain :feature:node -.-> :core:model :feature:node -.-> :core:navigation :feature:node -.-> :core:repository :feature:node -.-> :core:resources - :feature:node -.-> :core:service :feature:node -.-> :core:ui :feature:node -.-> :core:di - :feature:node -.-> :feature:map :feature:node -.-> :core:testing classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 56205802ca..050b1b94df 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -27,19 +27,14 @@ kotlin { commonMain.dependencies { implementation(libs.coil) implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.navigation) implementation(libs.meshtastic.protobufs) implementation(projects.core.repository) implementation(projects.core.resources) - implementation(projects.core.service) implementation(projects.core.ui) implementation(projects.core.di) - implementation(projects.feature.map) implementation(libs.markdown.renderer) implementation(libs.markdown.renderer.m3) diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml index 35e55d6d87..7db65a1758 100644 --- a/feature/node/detekt-baseline.xml +++ b/feature/node/detekt-baseline.xml @@ -2,17 +2,14 @@ + AbstractClassCanBeInterface:NodeMenuAction.kt:NodeMenuAction$NodeMenuAction ComposableParamOrder:BaseMetricChart.kt:@Composable @Suppress("LongMethod") fun <T> BaseMetricScreen ComposableParamOrder:DeviceMetrics.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun DeviceMetricsChart - ComposableParamOrder:ElevationInfo.kt:@Composable fun ElevationInfo ComposableParamOrder:EnvironmentCharts.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun EnvironmentMetricsChart ComposableParamOrder:HostMetricsChart.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable internal fun HostMetricsChart ComposableParamOrder:HostMetricsLog.kt:@Composable fun LogLine - ComposableParamOrder:LastHeardInfo.kt:@Composable fun LastHeardInfo ComposableParamOrder:NeighborInfoLog.kt:@OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun NeighborInfoLogScreen ComposableParamOrder:NodeDetailScreens.kt:@Composable fun NodeDetailScreen - ComposableParamOrder:NodeFilterTextField.kt:@Suppress("LongParameterList") @Composable fun NodeFilterTextField - ComposableParamOrder:NodeItem.kt:@Composable @Suppress("LongMethod") fun NodeItem ComposableParamOrder:PaxMetrics.kt:@Suppress("LongMethod") @Composable private fun PaxMetricsChart ComposableParamOrder:PowerMetrics.kt:@Suppress("LongMethod") @Composable private fun PowerMetricsChart ComposableParamOrder:SignalMetrics.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun SignalMetricsChart @@ -25,14 +22,12 @@ LambdaParameterEventTrailing:EnvironmentCharts.kt:onPointSelected: (Double) -> Unit LambdaParameterEventTrailing:HostMetricsChart.kt:onPointSelected: (Double) -> Unit LambdaParameterEventTrailing:NeighborInfoLog.kt:onNavigateUp: () -> Unit - LambdaParameterEventTrailing:NodeFilterTextField.kt:onToggleExcludeMqtt: () -> Unit LambdaParameterEventTrailing:PaxMetrics.kt:onPointSelected: (Double) -> Unit LambdaParameterEventTrailing:PowerMetrics.kt:onPointSelected: (Double) -> Unit LambdaParameterEventTrailing:SignalMetrics.kt:onPointSelected: (Double) -> Unit LambdaParameterEventTrailing:TracerouteChart.kt:onPointSelected: (Double) -> Unit - LambdaParameterInRestartableEffect:NodeDetailScreens.kt:onNavigate: (Route) -> Unit = {} + LongParameterList:CommonGetNodeDetailsUseCase.kt:CommonGetNodeDetailsUseCase MagicNumber:CompassViewModel.kt:CompassViewModel$180.0 - ModifierMissing:BaseMetricChart.kt:@Composable @Suppress("LongMethod") fun <T> BaseMetricScreen ModifierMissing:CommonCharts.kt:@Composable fun LegendIndicator ModifierMissing:DeviceMetrics.kt:@Suppress("LongMethod") @Composable fun DeviceMetricsScreen ModifierMissing:EnvironmentMetrics.kt:@Composable fun EnvironmentMetricsScreen @@ -44,7 +39,6 @@ ModifierMissing:PositionLogComponents.kt:@Composable @Suppress("LongMethod") fun PositionCard ModifierMissing:PositionLogScreens.kt:@Composable fun PositionLogScreen ModifierMissing:PowerMetrics.kt:@Suppress("LongMethod") @Composable fun PowerMetricsScreen - ModifierMissing:SignalMetrics.kt:@Suppress("LongMethod") @Composable fun SignalMetricsScreen ModifierNotUsedAtRoot:NeighborInfoLog.kt:modifier = modifier.fillMaxSize().padding(innerPadding) ModifierNotUsedAtRoot:TracerouteMapScreen.kt:modifier = modifier.fillMaxSize().padding(paddingValues) ModifierWithoutDefault:NodeDetailScreens.kt:modifier: Modifier @@ -55,11 +49,19 @@ MultipleEmitters:TracerouteLog.kt:@Composable private fun TracerouteCardMetrics MutableStateAutoboxing:CooldownOutlinedIconButton.kt:mutableStateOf(0f) MutableStateAutoboxing:TracerouteMapScreen.kt:mutableStateOf(0) + NoNameShadowing:AirQualityMetrics.kt:modifier + NoNameShadowing:AirQualityMetrics.kt:wb + NoNameShadowing:BaseMetricChart.kt:modifier + NoNameShadowing:DeviceMetrics.kt:wb + NoNameShadowing:EnvironmentMetricsState.kt:EnvironmentMetricsState${ !it.isNaN() && it != 0.0f } + NoNameShadowing:EnvironmentMetricsState.kt:EnvironmentMetricsState${ !it.isNaN() && it > 0f } + NoNameShadowing:EnvironmentMetricsState.kt:EnvironmentMetricsState${ !it.isNaN() } + NoNameShadowing:EnvironmentMetricsState.kt:EnvironmentMetricsState${ it != Int.MIN_VALUE } + NoNameShadowing:TelemetricActionsSection.kt:node ParameterNaming:BaseMetricChart.kt:onPointSelected: ((Double) -> Unit)? = null ParameterNaming:DeviceMetrics.kt:onPointSelected: (Double) -> Unit ParameterNaming:EnvironmentCharts.kt:onPointSelected: (Double) -> Unit ParameterNaming:HostMetricsChart.kt:onPointSelected: (Double) -> Unit - ParameterNaming:NodeFilterTextField.kt:onToggleShowIgnored: () -> Unit ParameterNaming:PaxMetrics.kt:onPointSelected: (Double) -> Unit ParameterNaming:PowerMetrics.kt:onPointSelected: (Double) -> Unit ParameterNaming:SignalMetrics.kt:onPointSelected: (Double) -> Unit @@ -77,13 +79,39 @@ PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLoadingPreview PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLocalPreview PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentRemotePreview - PreviewPublic:NodeListItemPreviews.kt:@PreviewLightDark @Composable fun NodeItemCompactActivePreview - PreviewPublic:NodeListItemPreviews.kt:@PreviewLightDark @Composable fun NodeItemCompactAllFieldsPreview - PreviewPublic:NodeListItemPreviews.kt:@PreviewLightDark @Composable fun NodeItemCompactMinimalPreview - PreviewPublic:NodeListItemPreviews.kt:@PreviewLightDark @Composable fun NodeItemCompleteActivePreview - PreviewPublic:NodeListItemPreviews.kt:@PreviewLightDark @Composable fun NodeItemCompletePreview TooGenericExceptionCaught:MetricsViewModel.kt:MetricsViewModel$e: Exception - TooGenericExceptionCaught:NodeManagementActions.kt:NodeManagementActions$ex: Exception + UnnecessaryLaunchedEffect:CompassBottomSheet.kt:LaunchedEffect + UnnecessaryLaunchedEffect:NodeDetailScreens.kt:LaunchedEffect + UnnecessaryLaunchedEffect:NodeListScreen.kt:LaunchedEffect + UnnecessaryLaunchedEffect:TracerouteMapScreen.kt:LaunchedEffect + UnusedPrivateProperty:MetricsViewModel.kt:MetricsViewModel$private val getNodeDetailsUseCase: GetNodeDetailsUseCase + UnusedPrivateProperty:NodeDetailViewModel.kt:NodeDetailViewModel$private val getNodeDetailsUseCase: GetNodeDetailsUseCase + UnusedPrivateProperty:NodeDetailViewModel.kt:NodeDetailViewModel$private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase + UnusedPrivateProperty:NodeDetailViewModel.kt:NodeDetailViewModel$private val savedStateHandle: SavedStateHandle + UnusedPrivateProperty:NodeListViewModel.kt:NodeListViewModel$private val connectionStateProvider: ConnectionStateProvider + UnusedPrivateProperty:NodeListViewModel.kt:NodeListViewModel$private val getFilteredNodesUseCase: GetFilteredNodesUseCase + UnusedPrivateProperty:NodeListViewModel.kt:NodeListViewModel$private val nodeRepository: NodeRepository + UnusedPrivateProperty:NodeListViewModel.kt:NodeListViewModel$private val radioInterfaceService: RadioInterfaceService + UseOrEmpty:AirQualityMetrics.kt:metricLabels[metric] ?: "" + UseOrEmpty:AirQualityMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:ChartStyling.kt:ChartStyling$label ?: "" + UseOrEmpty:CommonGetNodeDetailsUseCase.kt:CommonGetNodeDetailsUseCase$hw?.platformioTarget ?.takeIf { it.isNotBlank() } ?.let { deviceLinkRepository.getLinksForTarget(it) } ?: emptyList() + UseOrEmpty:DeviceMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:EnvironmentCharts.kt:colorToLabel[color] ?: "" + UseOrEmpty:EnvironmentCharts.kt:colorToUnit[color] ?: "" + UseOrEmpty:EnvironmentCharts.kt:metric?.let { unitSuffix(it, isFahrenheit, isImperial) } ?: "" + UseOrEmpty:EnvironmentCharts.kt:otherMetrics.map { unitSuffix(it, isFahrenheit, isImperial) }.distinct().singleOrNull() ?: "" + UseOrEmpty:EnvironmentCharts.kt:otherMetricsData[metric] ?: emptyList() + UseOrEmpty:EnvironmentMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:HostMetricsLog.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:LinkedCoordinatesItem.kt:node.validPosition?.altitude?.let { altitude -> val suffix = stringResource(Res.string.elevation_suffix) " • ${altitude.toElevationString(displayUnits)} $suffix" } ?: "" + UseOrEmpty:MetricsViewModel.kt:MetricsViewModel$state.value.node?.user?.long_name ?: "" + UseOrEmpty:NeighborInfoLog.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:PaxMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:PositionLogScreens.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:PowerMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:SignalMetrics.kt:state.node?.user?.long_name ?: "" + UseOrEmpty:TracerouteLog.kt:state.node?.user?.long_name ?: "" ViewModelForwarding:NodeDetailScreens.kt:NodeDetailScaffold( modifier = modifier, uiState = uiState, viewModel = viewModel, navigateToMessages = navigateToMessages, onNavigate = onNavigate, onNavigateUp = onNavigateUp, compassViewModel = compassViewModel, ) diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt index 0658b2661e..f869249960 100644 --- a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt @@ -16,13 +16,11 @@ */ package org.meshtastic.feature.node.compass -import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.location.Location import android.location.LocationManager import android.os.Looper -import androidx.core.content.ContextCompat import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationRequestCompat @@ -31,6 +29,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn import org.koin.core.annotation.Single +import org.meshtastic.core.common.hasLocationPermission import org.meshtastic.core.di.CoroutineDispatchers @Single @@ -46,7 +45,8 @@ class AndroidPhoneLocationProvider(private val context: Context, private val dis return@callbackFlow } - if (!hasLocationPermission()) { + // A compass bearing is useful on an approximate fix, so the coarse-only grant is enough here. + if (!context.hasLocationPermission(precise = false)) { trySend(PhoneLocationState(permissionGranted = false, providerEnabled = false)) close() return@callbackFlow @@ -123,12 +123,6 @@ class AndroidPhoneLocationProvider(private val context: Context, private val dis } .flowOn(dispatchers.io) - private fun hasLocationPermission(): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == - android.content.pm.PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == - android.content.pm.PackageManager.PERMISSION_GRANTED - private fun Location.toPhoneLocation() = PhoneLocation(latitude = latitude, longitude = longitude, altitude = altitude, timeMillis = time) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt index ff17aacc6e..070f13d2b6 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt @@ -36,7 +36,7 @@ data class CompassUiState( val bearing: Float? = null, val distanceText: String? = null, val bearingText: String? = null, - val lastUpdateText: String? = null, + val lastUpdateAgeSeconds: Long? = null, val positionTimeSec: Long? = null, // Epoch seconds for the target position (used for elapsed display) val warnings: List = emptyList(), val errorRadiusText: String? = null, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt index ed5a0be9e5..390109b9e6 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt @@ -47,8 +47,6 @@ import kotlin.math.sqrt private const val ALIGNMENT_TOLERANCE_DEGREES = 5f private const val FULL_CIRCLE_DEGREES = 360f private const val BEARING_FORMAT = "%.0f°" -private const val SECONDS_PER_HOUR = 3600 -private const val SECONDS_PER_MINUTE = 60 private const val HUNDRED = 100f private const val MILLIMETERS_PER_METER = 1000f @@ -125,7 +123,7 @@ class CompassViewModel( val distanceText = distanceMeters?.toDistanceString(current.displayUnits) val bearingText = bearingDegrees?.let { formatString(BEARING_FORMAT, it) } val isAligned = isAligned(trueHeading, bearingDegrees) - val lastUpdateText = targetPositionTimeSec?.let { formatElapsed(it) } + val lastUpdateAgeSeconds = targetPositionTimeSec?.let { maxOf(0, nowSeconds - it) } val angularErrorDeg = calculateAngularError(positionalAccuracyMeters, distanceMeters) val errorRadiusText = positionalAccuracyMeters?.toInt()?.let { "± ${it.toDistanceString(current.displayUnits)}" } @@ -137,7 +135,7 @@ class CompassViewModel( bearingText = bearingText, warnings = warnings, isAligned = isAligned, - lastUpdateText = lastUpdateText, + lastUpdateAgeSeconds = lastUpdateAgeSeconds, errorRadiusText = errorRadiusText, angularErrorDeg = angularErrorDeg, ) @@ -197,16 +195,6 @@ class CompassViewModel( return (baseHeading + declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES } - private fun formatElapsed(timestampSec: Long): String { - val nowSec = nowSeconds - val diff = maxOf(0, nowSec - timestampSec) - val hours = diff / SECONDS_PER_HOUR - val minutes = (diff % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE - val seconds = diff % SECONDS_PER_MINUTE - // Show a short elapsed string to match iOS behavior and avoid locale/format churn - return "${hours}h ${minutes}m ${seconds}s ago" - } - @Suppress("ReturnCount") private fun calculatePositionalAccuracyMeters(): Float? { val position = targetPositionProto ?: return null diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index d19b5136dc..5b7272b246 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier @@ -33,11 +34,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.model.DeviceVersion +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.Node import org.meshtastic.core.model.SessionStatus +import org.meshtastic.core.model.asDeviceVersion import org.meshtastic.core.repository.EventFirmwareRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration @@ -270,6 +271,7 @@ private fun FirmwareVersionItems( } @Composable +@ReadOnlyComposable private fun DeviceVersion.determineFirmwareStatusColor( latestStable: FirmwareRelease, latestAlpha: FirmwareRelease, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt index d84cf9455e..f92d01b842 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt @@ -64,9 +64,12 @@ import org.meshtastic.core.resources.compass_no_magnetometer import org.meshtastic.core.resources.compass_title import org.meshtastic.core.resources.compass_uncertainty import org.meshtastic.core.resources.compass_uncertainty_unknown +import org.meshtastic.core.resources.duration_ago import org.meshtastic.core.resources.elevation_suffix import org.meshtastic.core.resources.exchange_position +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.last_position_update +import org.meshtastic.core.ui.component.ElevationInfo import org.meshtastic.core.ui.icon.ErrorOutline import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.MyLocation @@ -149,9 +152,10 @@ fun CompassSheetContent( } } - uiState.lastUpdateText?.let { + uiState.lastUpdateAgeSeconds?.let { ageSeconds -> + val age = stringResource(Res.string.duration_ago, formatDuration(ageSeconds)) Text( - text = stringResource(Res.string.last_position_update) + ": $it", + text = stringResource(Res.string.last_position_update) + ": $age", style = MaterialTheme.typography.bodyMedium, ) // Quick way to re-request a fresh fix without leaving the compass sheet diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt index 740d76bfd0..ba50a1dff4 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,6 +38,7 @@ internal const val COOL_DOWN_TIME_MS = 30000L internal const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes @Composable +@NonRestartableComposable fun CooldownIconButton( onClick: () -> Unit, cooldownTimestamp: Long?, @@ -53,6 +55,7 @@ fun CooldownIconButton( ) @Composable +@NonRestartableComposable fun CooldownOutlinedIconButton( onClick: () -> Unit, cooldownTimestamp: Long?, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt index 9b993de090..7f54afca20 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt @@ -46,15 +46,20 @@ import coil3.compose.LocalPlatformContext import coil3.request.ImageRequest import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.HardwareSupportTier +import org.meshtastic.core.model.supportTier import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ic_unverified import org.meshtastic.core.resources.img_hw_unknown import org.meshtastic.core.resources.supported import org.meshtastic.core.resources.supported_by_community +import org.meshtastic.core.resources.supported_by_maker import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Verified +import org.meshtastic.core.ui.icon.Wrench import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusRed +import org.meshtastic.core.ui.theme.StatusColors.StatusSky /** * Device "hero" section showing the hardware image, device name, and support status. Used as the top section of the @@ -84,7 +89,7 @@ internal fun DeviceHeroSection( color = colorScheme.onSurface, ) Spacer(Modifier.height(4.dp)) - SupportStatusBadge(deviceHardware.activelySupported) + SupportStatusBadge(deviceHardware.supportTier) } } } @@ -102,27 +107,39 @@ private fun DeviceAvatar(bgColor: Long, deviceHardware: DeviceHardware, size: In } } +/** + * Names the rung beside its mark. The icon carries no description of its own: the label is the accessible name, so the + * rung is never conveyed by colour alone. + */ @Composable -private fun SupportStatusBadge(isSupported: Boolean) { +private fun SupportStatusBadge(tier: HardwareSupportTier) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { Icon( imageVector = - if (isSupported) { - MeshtasticIcons.Verified - } else { - org.jetbrains.compose.resources.vectorResource(Res.drawable.ic_unverified) + when (tier) { + HardwareSupportTier.SUPPORTED -> MeshtasticIcons.Verified + + HardwareSupportTier.MAKER -> MeshtasticIcons.Wrench + + HardwareSupportTier.COMMUNITY -> + org.jetbrains.compose.resources.vectorResource(Res.drawable.ic_unverified) }, contentDescription = null, modifier = Modifier.size(14.dp), - tint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed, + tint = + when (tier) { + HardwareSupportTier.SUPPORTED -> colorScheme.StatusGreen + HardwareSupportTier.MAKER -> colorScheme.StatusSky + HardwareSupportTier.COMMUNITY -> colorScheme.StatusRed + }, ) Spacer(Modifier.width(4.dp)) Text( text = - if (isSupported) { - stringResource(Res.string.supported) - } else { - stringResource(Res.string.supported_by_community) + when (tier) { + HardwareSupportTier.SUPPORTED -> stringResource(Res.string.supported) + HardwareSupportTier.MAKER -> stringResource(Res.string.supported_by_maker) + HardwareSupportTier.COMMUNITY -> stringResource(Res.string.supported_by_community) }, style = MaterialTheme.typography.labelSmall, color = colorScheme.onSurfaceVariant, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt deleted file mode 100644 index e239ab2810..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.distance -import org.meshtastic.core.ui.icon.Distance -import org.meshtastic.core.ui.icon.MeshtasticIcons - -@Composable -fun DistanceInfo( - distance: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Distance, - contentDescription = stringResource(Res.string.distance), - label = stringResource(Res.string.distance), - text = distance, - contentColor = contentColor, - ) -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt deleted file mode 100644 index 2cc985a8c2..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.common.util.MeasurementSystem -import org.meshtastic.core.model.util.toElevationString -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.altitude -import org.meshtastic.core.resources.elevation_suffix -import org.meshtastic.core.ui.icon.Elevation -import org.meshtastic.core.ui.icon.MeshtasticIcons - -@Composable -fun ElevationInfo( - modifier: Modifier = Modifier, - altitude: Int, - system: MeasurementSystem, - suffix: String = stringResource(Res.string.elevation_suffix), - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Elevation, - contentDescription = stringResource(Res.string.altitude), - label = stringResource(Res.string.altitude), - text = altitude.toElevationString(system) + " " + suffix, - contentColor = contentColor, - ) -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt index f66700fb9e..5804c85884 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mikepenz.markdown.m3.Markdown import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.download import org.meshtastic.core.resources.firmware_version diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt deleted file mode 100644 index 474e8baf2f..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.hops_away -import org.meshtastic.core.ui.icon.HopCount -import org.meshtastic.core.ui.icon.MeshtasticIcons - -@Composable -fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.HopCount, - contentDescription = stringResource(Res.string.hops_away), - label = stringResource(Res.string.hops_away), - text = hops.toString(), - contentColor = contentColor, - ) -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt deleted file mode 100644 index 47ddcb9aeb..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp - -private const val SIZE_ICON = 20 - -@Composable -fun IconInfo( - icon: ImageVector, - contentDescription: String, - modifier: Modifier = Modifier, - label: String? = null, - text: String? = null, - style: TextStyle = MaterialTheme.typography.labelMedium, - contentColor: Color = MaterialTheme.colorScheme.onSurface, - content: @Composable () -> Unit = {}, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Icon( - modifier = Modifier.size(SIZE_ICON.dp), - imageVector = icon, - contentDescription = contentDescription, - tint = contentColor, - ) - label?.let { Text(text = it, style = style, color = contentColor) } - text?.let { Text(text = it, style = style, color = contentColor) } - content() - } -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt deleted file mode 100644 index 7f977b1cac..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.resources.vectorResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.ic_antenna -import org.meshtastic.core.resources.node_sort_last_heard -import org.meshtastic.core.ui.util.formatAgo - -@Composable -fun LastHeardInfo( - modifier: Modifier = Modifier, - lastHeard: Int, - showLabel: Boolean = true, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = vectorResource(Res.drawable.ic_antenna), - contentDescription = stringResource(Res.string.node_sort_last_heard), - label = if (showLabel) stringResource(Res.string.node_sort_last_heard) else null, - text = formatAgo(lastHeard), - contentColor = contentColor, - ) -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt index ffb9e4ccde..6b49178c1a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt @@ -417,6 +417,29 @@ private fun NodeDetailsSectionWithDeviceHeroPreview() { } } +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun NodeDetailsSectionWithMakerDeviceHeroPreview() { + val node = previewData.mickeyMouse + // Flagship supportLevel with activelySupported false: the registry's shape for the first maker board. + val deviceHardware = + org.meshtastic.core.model.DeviceHardware( + displayName = "Axiometa Genesis Mini", + activelySupported = false, + isMaker = true, + supportLevel = 1, + images = listOf("axiometa-genesis-mini.svg"), + hwModel = 148, + hwModelSlug = "AXIOMETA_GENESIS_MINI", + ) + AppTheme { + Surface { + NodeDetailsSection(node = node, deviceHardware = deviceHardware, reportedTarget = "axiometa-genesis-mini") + } + } +} + @PreviewLightDark @Composable private fun DeviceLinksSectionPreview() { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index 3405c8ed55..1edb22398a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -61,7 +61,6 @@ import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.NodeSecurityIndicator -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.a11y_label_value import org.meshtastic.core.resources.copy @@ -69,6 +68,7 @@ import org.meshtastic.core.resources.details import org.meshtastic.core.resources.encryption_error import org.meshtastic.core.resources.encryption_error_text import org.meshtastic.core.resources.error +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.hops_away import org.meshtastic.core.resources.no_public_key import org.meshtastic.core.resources.no_public_key_text @@ -112,7 +112,6 @@ import org.meshtastic.core.ui.util.createClipEntry import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.proto.MeshPacket.TransportMechanism import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi @Composable fun NodeDetailsSection( @@ -291,7 +290,7 @@ private fun UserAndUptimeRow(node: Node) { if (uptimeSeconds != null && uptimeSeconds > 0) { InfoItem( label = stringResource(Res.string.uptime), - value = formatUptime(uptimeSeconds), + value = formatDuration(uptimeSeconds.toLong()), icon = MeshtasticIcons.ArrowCircleUp, modifier = Modifier.weight(1f), ) @@ -376,7 +375,7 @@ private fun SecurityRow(node: Node, isLocal: Boolean) { } } -@OptIn(ExperimentalFoundationApi::class, ExperimentalEncodingApi::class) +@OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "MagicNumber") @Composable private fun PublicKeyItem(publicKeyBytes: ByteArray) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBar.kt similarity index 77% rename from feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBar.kt index 8a3ed94cff..d36d64f16e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBar.kt @@ -20,25 +20,22 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -48,18 +45,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusEvent -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.clear -import org.meshtastic.core.resources.desc_node_filter_clear import org.meshtastic.core.resources.node_filter_exclude_infrastructure import org.meshtastic.core.resources.node_filter_exclude_mqtt import org.meshtastic.core.resources.node_filter_exclude_unheard @@ -74,34 +65,42 @@ import org.meshtastic.core.resources.node_filter_show_ignored import org.meshtastic.core.resources.node_filter_title import org.meshtastic.core.resources.node_sort_button import org.meshtastic.core.resources.node_sort_title +import org.meshtastic.core.ui.component.MeshtasticSearchBar import org.meshtastic.core.ui.component.SignedNodeIcon -import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.Lock import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Search import org.meshtastic.core.ui.icon.Sort import org.meshtastic.core.ui.theme.StatusColors.StatusGreen +/** + * The node list's search field: a Material 3 [MeshtasticSearchBar] whose expanded surface shows the matching nodes, + * with the sort and filter menu as its trailing action. + * + * The "showing ignored nodes" banner sits under the collapsed bar rather than inside it: it reports a filter that is + * already applied to the list below, not a search result. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun NodeFilterTextField( +fun NodeFilterSearchBar( filterText: String, onTextChange: (String) -> Unit, currentSortOption: NodeSortOption, onSortSelect: (NodeSortOption) -> Unit, toggles: NodeFilterToggles, modifier: Modifier = Modifier, + searchResults: @Composable ColumnScope.() -> Unit = {}, ) { - Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) { - Row { - NodeFilterTextField(filterText = filterText, onTextChange = onTextChange, modifier = Modifier.weight(1f)) - - NodeSortButton( - modifier = Modifier.align(Alignment.CenterVertically), - currentSortOption = currentSortOption, - onSortSelect = onSortSelect, - toggles = toggles, - ) - } + Column(modifier = modifier) { + MeshtasticSearchBar( + query = filterText, + onQueryChange = onTextChange, + placeholder = stringResource(Res.string.node_filter_placeholder), + inputFieldTag = NODE_FILTER_SEARCH_BAR_INPUT_FIELD_TAG, + trailingActions = { + NodeSortButton(currentSortOption = currentSortOption, onSortSelect = onSortSelect, toggles = toggles) + }, + expandedContent = searchResults, + ) if (toggles.showIgnored) { Box( modifier = @@ -122,6 +121,9 @@ fun NodeFilterTextField( } } +/** Tag for the collapsed node search field, so a test can target it rather than the expanded overlay's copy. */ +const val NODE_FILTER_SEARCH_BAR_INPUT_FIELD_TAG = "NodeFilterSearchBarInputField" + data class NodeFilterToggles( val includeUnknown: Boolean, val onToggleIncludeUnknown: () -> Unit, @@ -144,50 +146,6 @@ data class NodeFilterToggles( val onToggleOnlyEncrypted: () -> Unit, ) -@Composable -private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier) { - val focusManager = LocalFocusManager.current - var isFocused by remember { mutableStateOf(false) } - - OutlinedTextField( - modifier = modifier.defaultMinSize(minHeight = 48.dp).onFocusEvent { isFocused = it.isFocused }, - value = filterText, - placeholder = { - Text( - text = stringResource(Res.string.node_filter_placeholder), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.35F), - ) - }, - leadingIcon = { - Icon(MeshtasticIcons.Search, contentDescription = stringResource(Res.string.node_filter_placeholder)) - }, - onValueChange = onTextChange, - trailingIcon = { - if (filterText.isNotEmpty() || isFocused) { - val clearLabel = stringResource(Res.string.clear) - Icon( - MeshtasticIcons.Close, - contentDescription = stringResource(Res.string.desc_node_filter_clear), - modifier = - Modifier.clickable( - onClickLabel = clearLabel, - role = Role.Button, - onClick = { - onTextChange("") - focusManager.clearFocus() - }, - ), - ) - } - }, - textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onBackground), - maxLines = 1, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - ) -} - @Suppress("LongMethod") @Composable private fun NodeSortButton( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBarPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBarPreviews.kt new file mode 100644 index 0000000000..c8bf7b53c9 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterSearchBarPreviews.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.core.ui.theme.AppTheme + +private val previewToggles = + NodeFilterToggles( + includeUnknown = true, + onToggleIncludeUnknown = {}, + excludeInfrastructure = false, + onToggleExcludeInfrastructure = {}, + onlyOnline = false, + onToggleOnlyOnline = {}, + onlyDirect = false, + onToggleOnlyDirect = {}, + showIgnored = false, + onToggleShowIgnored = {}, + ignoredNodeCount = 0, + excludeUnheard = false, + onToggleExcludeUnheard = {}, + excludeMqtt = false, + onToggleExcludeMqtt = {}, + onlySigned = false, + onToggleOnlySigned = {}, + onlyEncrypted = false, + onToggleOnlyEncrypted = {}, + ) + +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun NodeFilterSearchBarEmptyPreview() { + AppTheme { + Surface { + NodeFilterSearchBar( + filterText = "", + onTextChange = {}, + currentSortOption = NodeSortOption.LAST_HEARD, + onSortSelect = {}, + toggles = previewToggles, + modifier = Modifier.padding(16.dp), + ) + } + } +} + +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun NodeFilterSearchBarWithQueryPreview() { + AppTheme { + Surface { + NodeFilterSearchBar( + filterText = "kolsås", + onTextChange = {}, + currentSortOption = NodeSortOption.LAST_HEARD, + onSortSelect = {}, + toggles = previewToggles, + modifier = Modifier.padding(16.dp), + ) + } + } +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt deleted file mode 100644 index ac11d9a887..0000000000 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("TooManyFunctions") - -package org.meshtastic.feature.node.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.env_metrics_log -import org.meshtastic.core.resources.hardware_model -import org.meshtastic.core.resources.humidity -import org.meshtastic.core.resources.iaq -import org.meshtastic.core.resources.node_id -import org.meshtastic.core.resources.pax -import org.meshtastic.core.resources.pax_metrics_log -import org.meshtastic.core.resources.role -import org.meshtastic.core.resources.soil_moisture -import org.meshtastic.core.resources.soil_temperature -import org.meshtastic.core.resources.temperature -import org.meshtastic.core.ui.icon.AirQuality -import org.meshtastic.core.ui.icon.ElectricPower -import org.meshtastic.core.ui.icon.HardwareModel -import org.meshtastic.core.ui.icon.Humidity -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.NodeId -import org.meshtastic.core.ui.icon.PeopleCount -import org.meshtastic.core.ui.icon.Role -import org.meshtastic.core.ui.icon.SoilMoisture -import org.meshtastic.core.ui.icon.Temperature - -@Composable -fun TemperatureInfo( - temp: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Temperature, - contentDescription = stringResource(Res.string.env_metrics_log), - label = stringResource(Res.string.temperature), - text = temp, - contentColor = contentColor, - ) -} - -@Composable -fun HumidityInfo( - humidity: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Humidity, - contentDescription = stringResource(Res.string.env_metrics_log), - label = stringResource(Res.string.humidity), - text = humidity, - contentColor = contentColor, - ) -} - -@Composable -fun SoilTemperatureInfo( - temp: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.SoilMoisture, - contentDescription = stringResource(Res.string.env_metrics_log), - label = stringResource(Res.string.soil_temperature), - text = temp, - contentColor = contentColor, - ) -} - -@Composable -fun SoilMoistureInfo( - moisture: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.SoilMoisture, - contentDescription = stringResource(Res.string.env_metrics_log), - label = stringResource(Res.string.soil_moisture), - text = moisture, - contentColor = contentColor, - ) -} - -@Composable -fun PaxcountInfo( - pax: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.PeopleCount, - contentDescription = stringResource(Res.string.pax_metrics_log), - label = stringResource(Res.string.pax), - text = pax, - contentColor = contentColor, - ) -} - -@Composable -fun AirQualityInfo( - iaq: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.AirQuality, - contentDescription = stringResource(Res.string.env_metrics_log), - label = stringResource(Res.string.iaq), - text = iaq, - contentColor = contentColor, - ) -} - -@Composable -fun PowerInfo( - value: String, - modifier: Modifier = Modifier, - label: String? = null, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.ElectricPower, - contentDescription = stringResource(Res.string.env_metrics_log), - label = label, - text = value, - contentColor = contentColor, - ) -} - -@Composable -fun HardwareInfo( - hwModel: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.HardwareModel, - contentDescription = stringResource(Res.string.hardware_model), - text = hwModel, - style = MaterialTheme.typography.labelSmall, - contentColor = contentColor, - ) -} - -@Composable -fun RoleInfo(role: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.Role, - contentDescription = stringResource(Res.string.role), - text = role, - style = MaterialTheme.typography.labelSmall, - contentColor = contentColor, - ) -} - -@Composable -fun NodeIdInfo(id: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.NodeId, - contentDescription = stringResource(Res.string.node_id), - text = id, - style = MaterialTheme.typography.labelSmall, - contentColor = contentColor, - ) -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt index 877eaa7816..0bb67c73df 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.loading diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt index dccbae94f8..c5ba396437 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt @@ -36,13 +36,13 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.MeasurementSystem -import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.Route import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.details import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.core.ui.component.SharedContactDialog +import org.meshtastic.core.ui.component.ShareContactDialog import org.meshtastic.core.ui.util.ActiveWhileStarted import org.meshtastic.feature.node.compass.CompassUiState import org.meshtastic.feature.node.compass.CompassViewModel @@ -71,7 +71,8 @@ fun NodeDetailScreen( ) { SideEffect(nodeId) { viewModel.start(nodeId) } val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(viewModel) { viewModel.navigationEvents.collect { onNavigate(it) } } + val currentOnNavigate by rememberUpdatedState(onNavigate) + LaunchedEffect(viewModel) { viewModel.navigationEvents.collect { currentOnNavigate(it) } } NodeDetailScaffold( modifier = modifier, uiState = uiState, @@ -190,7 +191,7 @@ private fun NodeDetailOverlays( } when (overlay) { - is NodeDetailOverlay.SharedContact -> node?.let { SharedContactDialog(it, onDismiss, isOwnContact = isLocal) } + is NodeDetailOverlay.SharedContact -> node?.let { ShareContactDialog(it, onDismiss, isOwnContact = isLocal) } is NodeDetailOverlay.FirmwareReleaseInfo -> NodeDetailBottomSheet(onDismiss) { FirmwareReleaseSheetContent(firmwareRelease = overlay.release) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt index c1b2c6330c..44b11a31ee 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt @@ -28,9 +28,9 @@ import org.koin.core.annotation.Single import org.meshtastic.core.common.util.LocaleUnitsProvider import org.meshtastic.core.common.util.MeasurementSystem import org.meshtastic.core.common.util.TemperatureUnit -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.DeviceLink +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.MeshLog import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index 0f37b3c935..679951ebc1 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button @@ -56,7 +57,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow @@ -66,6 +66,7 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeListDensity +import org.meshtastic.core.model.excludes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.channel_invalid import org.meshtastic.core.resources.hop_histogram_title @@ -80,12 +81,14 @@ import org.meshtastic.core.resources.nodes_unheard_banner_one import org.meshtastic.core.resources.nodes_unheard_keep import org.meshtastic.core.resources.nodes_unheard_remove import org.meshtastic.core.resources.set_up_connection +import org.meshtastic.core.ui.component.EmptyState +import org.meshtastic.core.ui.component.ListScrollbar import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.MeshtasticImportFAB import org.meshtastic.core.ui.component.NodeItem import org.meshtastic.core.ui.component.NodeItemCompact import org.meshtastic.core.ui.component.ScrollToTopEvent -import org.meshtastic.core.ui.component.SharedContactDialog +import org.meshtastic.core.ui.component.ShareContactDialog import org.meshtastic.core.ui.component.smartScrollToTop import org.meshtastic.core.ui.icon.BarChart import org.meshtastic.core.ui.icon.Info @@ -98,17 +101,21 @@ import org.meshtastic.core.ui.util.parseDeepLinkOrInvalid import org.meshtastic.feature.node.component.LocalNodeContextMenu import org.meshtastic.feature.node.component.NodeContextMenu import org.meshtastic.feature.node.component.NodeCountSummary -import org.meshtastic.feature.node.component.NodeFilterTextField +import org.meshtastic.feature.node.component.NodeFilterSearchBar import org.meshtastic.feature.node.component.NodeFilterToggles import org.meshtastic.feature.node.component.NodeHopHistogramSheet import org.meshtastic.feature.node.component.NodeListHelp +import org.meshtastic.proto.ExcludedModules /** * design#115: status message editing is offered on the connected local node only, and only where the firmware has the * module — absent, never disabled, everywhere else. */ internal fun canEditStatusMessage(node: Node, ourNode: Node?, connectionState: ConnectionState): Boolean = - node.num == ourNode?.num && connectionState == ConnectionState.Connected && node.capabilities.supportsStatusMessage + node.num == ourNode?.num && + connectionState == ConnectionState.Connected && + node.capabilities.supportsStatusMessage && + !node.metadata.excludes(ExcludedModules.STATUSMESSAGE_CONFIG) @Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalFoundationApi::class) @@ -189,7 +196,89 @@ fun NodeListScreen( var showShareContact by remember { mutableStateOf(false) } if (showShareContact) { - SharedContactDialog(contact = ourNode, onDismiss = { showShareContact = false }, isOwnContact = true) + ShareContactDialog(contact = ourNode, onDismiss = { showShareContact = false }, isOwnContact = true) + } + + // One row renderer, used by the list itself and by the search bar's expanded results, so the two can never drift. + // It stays a LazyItemScope lambda rather than a composable of its own because animateItem() needs that receiver. + val nodeRow: @Composable LazyItemScope.(Node) -> Unit = { node -> + var expanded by remember { mutableStateOf(false) } + + Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { + val isThisNode = node.num == ourNode?.num + val canEditStatus = canEditStatusMessage(node, ourNode, connectionState) + // Our own node only earns a long press while it has something to offer, or it opens an empty menu. + val longClick = + if (!isThisNode || canEditStatus) { + { expanded = true } + } else { + null + } + + val isActive = remember(activeNodeId, node.num) { activeNodeId == node.num } + + when (density) { + NodeListDensity.COMPLETE -> + NodeItem( + modifier = Modifier.animateItem(), + thisNode = ourNode, + thatNode = node, + distanceUnits = state.distanceUnits, + tempInFahrenheit = state.tempInFahrenheit, + onClick = { navigateToNodeDetails(node.num) }, + onLongClick = longClick, + connectionState = connectionState, + deviceType = deviceType, + isActive = isActive, + showTelemetry = showTelemetry, + deviceImageUrl = deviceImageUrls[node.user.hw_model.value], + ) + + NodeListDensity.COMPACT -> + NodeItemCompact( + modifier = Modifier.animateItem(), + thisNode = ourNode, + thatNode = node, + distanceUnits = state.distanceUnits, + onClick = { navigateToNodeDetails(node.num) }, + onLongClick = longClick, + isActive = isActive, + showPower = showPower, + showLastHeard = showLastHeard, + lastHeardIsRelative = lastHeardIsRelative, + showLocation = showLocation, + showHops = showHops, + showSignal = showSignal, + showChannel = showChannel, + showRole = showRole, + showTelemetry = showTelemetry, + tempInFahrenheit = state.tempInFahrenheit, + deviceImageUrl = deviceImageUrls[node.user.hw_model.value], + ) + } + if (canEditStatus) { + LocalNodeContextMenu( + expanded = expanded, + onUpdateStatus = onEditStatusMessage, + onDismiss = { expanded = false }, + ) + } else if (!isThisNode) { + NodeContextMenu( + expanded = expanded, + node = node, + onFavorite = { viewModel.favoriteNode(node) }, + onMute = { viewModel.muteNode(node) }, + onMessage = { + val route = viewModel.getDirectMessageRoute(node) + navigateToMessages(route) + }, + onTraceRoute = { viewModel.traceRoute(node) }, + onIgnore = { viewModel.ignoreNode(node) }, + onRemove = { viewModel.removeNode(node) }, + onDismiss = { expanded = false }, + ) + } + } } Scaffold( @@ -263,12 +352,27 @@ fun NodeListScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp), ) val filterPrefs = viewModel.nodeFilterPreferences - NodeFilterTextField( + NodeFilterSearchBar( filterText = state.filter.filterText, onTextChange = { viewModel.nodeFilterText = it }, currentSortOption = state.sort, onSortSelect = viewModel::setSortOption, modifier = Modifier.fillMaxWidth(), + searchResults = { + // The full-screen expanded bar hides the sticky header's counts. + LazyColumn(modifier = Modifier.fillMaxSize()) { + item { + NodeCountSummary( + onlineCount = onlineNodeCount, + shownCount = nodes.size, + totalCount = totalNodeCount, + modifier = + Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + ) + } + items(nodes, key = { it.num }, itemContent = nodeRow) + } + }, toggles = NodeFilterToggles( includeUnknown = state.filter.includeUnknown, @@ -295,86 +399,7 @@ fun NodeListScreen( } } - items(nodes, key = { it.num }) { node -> - var expanded by remember { mutableStateOf(false) } - - Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { - val isThisNode = node.num == ourNode?.num - val canEditStatus = canEditStatusMessage(node, ourNode, connectionState) - // Our own node only earns a long press while it has something to offer, or it opens an - // empty menu. - val longClick = - if (!isThisNode || canEditStatus) { - { expanded = true } - } else { - null - } - - val isActive = remember(activeNodeId, node.num) { activeNodeId == node.num } - - when (density) { - NodeListDensity.COMPLETE -> - NodeItem( - modifier = Modifier.animateItem(), - thisNode = ourNode, - thatNode = node, - distanceUnits = state.distanceUnits, - tempInFahrenheit = state.tempInFahrenheit, - onClick = { navigateToNodeDetails(node.num) }, - onLongClick = longClick, - connectionState = connectionState, - deviceType = deviceType, - isActive = isActive, - showTelemetry = showTelemetry, - deviceImageUrl = deviceImageUrls[node.user.hw_model.value], - ) - - NodeListDensity.COMPACT -> - NodeItemCompact( - modifier = Modifier.animateItem(), - thisNode = ourNode, - thatNode = node, - distanceUnits = state.distanceUnits, - onClick = { navigateToNodeDetails(node.num) }, - onLongClick = longClick, - isActive = isActive, - showPower = showPower, - showLastHeard = showLastHeard, - lastHeardIsRelative = lastHeardIsRelative, - showLocation = showLocation, - showHops = showHops, - showSignal = showSignal, - showChannel = showChannel, - showRole = showRole, - showTelemetry = showTelemetry, - tempInFahrenheit = state.tempInFahrenheit, - deviceImageUrl = deviceImageUrls[node.user.hw_model.value], - ) - } - if (canEditStatus) { - LocalNodeContextMenu( - expanded = expanded, - onUpdateStatus = onEditStatusMessage, - onDismiss = { expanded = false }, - ) - } else if (!isThisNode) { - NodeContextMenu( - expanded = expanded, - node = node, - onFavorite = { viewModel.favoriteNode(node) }, - onMute = { viewModel.muteNode(node) }, - onMessage = { - val route = viewModel.getDirectMessageRoute(node) - navigateToMessages(route) - }, - onTraceRoute = { viewModel.traceRoute(node) }, - onIgnore = { viewModel.ignoreNode(node) }, - onRemove = { viewModel.removeNode(node) }, - onDismiss = { expanded = false }, - ) - } - } - } + items(nodes, key = { it.num }, itemContent = nodeRow) if (nodes.isEmpty() && !state.filter.isActive) { item { NodeListEmptyState( @@ -386,6 +411,7 @@ fun NodeListScreen( } item { Spacer(modifier = Modifier.height(88.dp)) } } + ListScrollbar(listState) } } } @@ -416,36 +442,18 @@ private fun NodeListEmptyState( stringResource(Res.string.nodes_empty_disconnected_hint), ) } - Column( - modifier = modifier.padding(horizontal = 32.dp, vertical = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = hint, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - if (!isConnected) { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onNavigateToConnections) { Text(stringResource(Res.string.set_up_connection)) } - } - } + EmptyState( + icon = icon, + title = title, + supportingText = hint, + modifier = modifier, + action = + if (isConnected) { + null + } else { + { Button(onClick = onNavigateToConnections) { Text(stringResource(Res.string.set_up_connection)) } } + }, + ) } /** diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/ChartGaps.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/ChartGaps.kt new file mode 100644 index 0000000000..065f878d04 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/ChartGaps.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import kotlin.time.Duration.Companion.minutes + +private const val GAP_MEDIAN_MULTIPLIER = 3 +private val MIN_GAP_SECONDS = 5.minutes.inWholeSeconds + +/** + * Splits [items] into runs that a chart should draw as separate lines, cutting wherever consecutive readings are more + * than the gap threshold apart. [items] must be sorted ascending by [timeSeconds]. + * + * The threshold is [GAP_MEDIAN_MULTIPLIER] times the median spacing, never below [MIN_GAP_SECONDS], so it follows the + * node's own reporting interval. + */ +internal fun splitAtGaps(items: List, timeSeconds: (T) -> Int): List> { + if (items.size < 2) return listOf(items).filter { it.isNotEmpty() } + val deltas = items.zipWithNext { a, b -> (timeSeconds(b) - timeSeconds(a)).toLong() } + val sorted = deltas.sorted() + val mid = sorted.size / 2 + val median = if (sorted.size % 2 == 0) (sorted[mid - 1] + sorted[mid]) / 2 else sorted[mid] + val threshold = maxOf(median * GAP_MEDIAN_MULTIPLIER, MIN_GAP_SECONDS) + val runs = mutableListOf(mutableListOf(items.first())) + items.zipWithNext().forEachIndexed { index, (_, next) -> + if (deltas[index] > threshold) runs.add(mutableListOf(next)) else runs.last().add(next) + } + return runs +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt index 4a7cc5a40a..f359795b18 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt @@ -62,7 +62,6 @@ import org.meshtastic.core.common.util.formatString import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.air_util_definition import org.meshtastic.core.resources.air_utilization @@ -74,6 +73,7 @@ import org.meshtastic.core.resources.device_metrics_log import org.meshtastic.core.resources.device_metrics_numeric_value import org.meshtastic.core.resources.device_metrics_percent_value import org.meshtastic.core.resources.device_metrics_voltage_value +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.uptime import org.meshtastic.core.resources.voltage import org.meshtastic.core.ui.component.MaterialBatteryInfo @@ -486,7 +486,11 @@ private fun DeviceMetricsCard( } Text( text = - formatString(labelValueTemplate, uptimeLabel, formatUptime(deviceMetrics?.uptime_seconds ?: 0)), + formatString( + labelValueTemplate, + uptimeLabel, + formatDuration((deviceMetrics?.uptime_seconds ?: 0).toLong()), + ), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, ) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt index a7fcb08cb1..29753a10e9 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt @@ -227,11 +227,10 @@ fun EnvironmentMetricsChart( } val colorToLabel = allLegendData.associate { it.color to legendLabel(it) } - val colorToUnit = - allLegendData.associate { legend -> - val metric = legend.metricKey as? Environment - legend.color to (metric?.let { unitSuffix(it, isFahrenheit, isImperial) } ?: "") - } + val colorToUnit = allLegendData.associate { legend -> + val metric = legend.metricKey as? Environment + legend.color to (metric?.let { unitSuffix(it, isFahrenheit, isImperial) } ?: "") + } val showPressure = shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] && Environment.BAROMETRIC_PRESSURE !in hiddenMetrics @@ -274,7 +273,7 @@ fun EnvironmentMetricsChart( lineModel { series( x = pressureData.map { it.time }, - y = pressureData.map { Environment.BAROMETRIC_PRESSURE.getValue(it)!! }, + y = pressureData.mapNotNull { Environment.BAROMETRIC_PRESSURE.getValue(it) }, ) } } @@ -285,7 +284,7 @@ fun EnvironmentMetricsChart( lineModel { series( x = metricData.map { it.time }, - y = metricData.map { chartValue(metric, it, isImperial)!! }, + y = metricData.mapNotNull { chartValue(metric, it, isImperial) }, ) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index 4f832a4570..4d9ca5bdc3 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -188,16 +188,15 @@ private fun TemperatureDisplay( @Composable private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) { - val hasHumidity = envMetrics.relative_humidity?.let { !it.isNaN() } == true - val hasPressure = envMetrics.barometric_pressure?.let { !it.isNaN() && it > 0 } == true + val humidity = envMetrics.relative_humidity?.takeUnless { it.isNaN() } + val pressure = envMetrics.barometric_pressure?.takeIf { !it.isNaN() && it > 0 } - if (hasHumidity || hasPressure) { + if (humidity != null || pressure != null) { Row( modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - if (hasHumidity) { - val humidity = envMetrics.relative_humidity!! + if (humidity != null) { Row(verticalAlignment = Alignment.CenterVertically) { MetricIndicator(Environment.HUMIDITY.color) Spacer(Modifier.width(4.dp)) @@ -212,8 +211,7 @@ private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.prot ) } } - if (hasPressure) { - val pressure = envMetrics.barometric_pressure!! + if (pressure != null) { Row(verticalAlignment = Alignment.CenterVertically) { MetricIndicator(Environment.BAROMETRIC_PRESSURE.color) Spacer(Modifier.width(4.dp)) @@ -285,13 +283,12 @@ private fun SoilMetricsDisplay( @Composable private fun LuxUVLuxDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) { - val hasLux = envMetrics.lux != null && !envMetrics.lux!!.isNaN() - val hasUvLux = envMetrics.uv_lux != null && !envMetrics.uv_lux!!.isNaN() + val luxValue = envMetrics.lux?.takeUnless { it.isNaN() } + val uvLuxValue = envMetrics.uv_lux?.takeUnless { it.isNaN() } - if (hasLux || hasUvLux) { + if (luxValue != null || uvLuxValue != null) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (hasLux) { - val luxValue = envMetrics.lux!! + if (luxValue != null) { Row(verticalAlignment = Alignment.CenterVertically) { MetricIndicator(Environment.LUX.color) Spacer(Modifier.width(4.dp)) @@ -302,8 +299,7 @@ private fun LuxUVLuxDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) ) } } - if (hasUvLux) { - val uvLuxValue = envMetrics.uv_lux!! + if (uvLuxValue != null) { Row(verticalAlignment = Alignment.CenterVertically) { MetricIndicator(Environment.UV_LUX.color) Spacer(Modifier.width(4.dp)) @@ -320,21 +316,19 @@ private fun LuxUVLuxDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) @Composable private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) { - val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage!!.isNaN() - val hasCurrent = envMetrics.current != null && !envMetrics.current!!.isNaN() + val voltage = envMetrics.voltage?.takeUnless { it.isNaN() } + val currentValue = envMetrics.current?.takeUnless { it.isNaN() } - if (hasVoltage || hasCurrent) { + if (voltage != null || currentValue != null) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (hasVoltage) { - val voltage = envMetrics.voltage!! + if (voltage != null) { Text( text = "${stringResource(Res.string.voltage)} ${MetricFormatter.voltage(voltage)}", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, ) } - if (hasCurrent) { - val currentValue = envMetrics.current!! + if (currentValue != null) { Text( text = "${stringResource( @@ -404,38 +398,38 @@ private fun RadiationDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics @Composable private fun WindDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics, isImperial: Boolean) { - val hasSpeed = envMetrics.wind_speed != null && !envMetrics.wind_speed!!.isNaN() - val hasGust = envMetrics.wind_gust != null && !envMetrics.wind_gust!!.isNaN() - val hasLull = envMetrics.wind_lull != null && !envMetrics.wind_lull!!.isNaN() + val speed = envMetrics.wind_speed?.takeUnless { it.isNaN() } + val gust = envMetrics.wind_gust?.takeUnless { it.isNaN() } + val lull = envMetrics.wind_lull?.takeUnless { it.isNaN() } - if (hasSpeed || hasGust || hasLull) { + if (speed != null || gust != null || lull != null) { Column(modifier = Modifier.fillMaxWidth()) { - if (hasSpeed) WindSpeedRow(envMetrics, isImperial) - if (hasGust || hasLull) WindGustLullRow(envMetrics, isImperial, hasGust, hasLull) + if (speed != null) WindSpeedRow(speed, envMetrics.wind_direction, isImperial) + if (gust != null || lull != null) WindGustLullRow(gust, lull, isImperial) } } } @Composable -private fun WindSpeedRow(envMetrics: org.meshtastic.proto.EnvironmentMetrics, isImperial: Boolean) { +private fun WindSpeedRow(speed: Float, direction: Int?, isImperial: Boolean) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(verticalAlignment = Alignment.CenterVertically) { MetricIndicator(Environment.WIND_SPEED.color) Spacer(Modifier.width(4.dp)) val dirText = - if (envMetrics.wind_direction != null) { + if (direction != null) { formatString( "%s %s (%s %d°)", stringResource(Res.string.wind_speed), - MetricFormatter.windSpeed(envMetrics.wind_speed!!, isImperial), + MetricFormatter.windSpeed(speed, isImperial), stringResource(Res.string.wind_direction), - envMetrics.wind_direction!!, + direction, ) } else { formatString( "%s %s", stringResource(Res.string.wind_speed), - MetricFormatter.windSpeed(envMetrics.wind_speed!!, isImperial), + MetricFormatter.windSpeed(speed, isImperial), ) } Text( @@ -448,28 +442,23 @@ private fun WindSpeedRow(envMetrics: org.meshtastic.proto.EnvironmentMetrics, is } @Composable -private fun WindGustLullRow( - envMetrics: org.meshtastic.proto.EnvironmentMetrics, - isImperial: Boolean, - hasGust: Boolean, - hasLull: Boolean, -) { +private fun WindGustLullRow(gust: Float?, lull: Float?, isImperial: Boolean) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (hasGust) { + if (gust != null) { Text( text = "${stringResource(Res.string.wind_gust)} ${ - MetricFormatter.windSpeed(envMetrics.wind_gust!!, isImperial) + MetricFormatter.windSpeed(gust, isImperial) }", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, ) } - if (hasLull) { + if (lull != null) { Text( text = "${stringResource(Res.string.wind_lull)} ${ - MetricFormatter.windSpeed(envMetrics.wind_lull!!, isImperial) + MetricFormatter.windSpeed(lull, isImperial) }", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, @@ -480,27 +469,27 @@ private fun WindGustLullRow( @Composable private fun RainfallDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics, isImperial: Boolean) { - val has1h = envMetrics.rainfall_1h != null && !envMetrics.rainfall_1h!!.isNaN() - val has24h = envMetrics.rainfall_24h != null && !envMetrics.rainfall_24h!!.isNaN() + val rainfall1h = envMetrics.rainfall_1h?.takeUnless { it.isNaN() } + val rainfall24h = envMetrics.rainfall_24h?.takeUnless { it.isNaN() } - if (has1h || has24h) { + if (rainfall1h != null || rainfall24h != null) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (has1h) { + if (rainfall1h != null) { Text( text = "${stringResource( Res.string.rainfall_1h, - )} ${MetricFormatter.rainfall(envMetrics.rainfall_1h!!, isImperial)}", + )} ${MetricFormatter.rainfall(rainfall1h, isImperial)}", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, ) } - if (has24h) { + if (rainfall24h != null) { Text( text = "${stringResource( Res.string.rainfall_24h, - )} ${MetricFormatter.rainfall(envMetrics.rainfall_24h!!, isImperial)}", + )} ${MetricFormatter.rainfall(rainfall24h, isImperial)}", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge, ) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsChart.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsChart.kt index f8cfc219ae..4feac4d34e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsChart.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsChart.kt @@ -29,6 +29,8 @@ import com.patrykandpatrick.vico.compose.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.compose.cartesian.data.CartesianLayerRangeProvider import com.patrykandpatrick.vico.compose.cartesian.data.lineModel import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer +import org.meshtastic.core.common.util.BYTES_PER_MEGABYTE +import org.meshtastic.core.common.util.formatMegabytes import org.meshtastic.core.common.util.formatString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.free_memory @@ -123,10 +125,13 @@ internal fun buildHostMetricsChartData(data: List): HostMetricsChartD telemetry.host_metrics ?.freemem_bytes ?.takeIf { it > 0 } - ?.let { HostMetricsChartPoint(time = telemetry.time, value = it.toDouble() / BYTES_IN_MB) } + ?.let { HostMetricsChartPoint(time = telemetry.time, value = it.toDouble() / BYTES_PER_MEGABYTE) } }, ) +/** Free memory is plotted in megabytes; one unit for every tick keeps the axis comparable. */ +internal fun freeMemoryChartLabel(megabytes: Double): String = formatMegabytes(megabytes, 0) + /** * Vico chart composable that renders load averages (1m, 5m, 15m) and free memory as dual-axis line series: load on the * start axis (fixed min 0), free memory in MB on the end axis. @@ -186,7 +191,7 @@ internal fun HostMetricsChart( load1Color -> formatString("L1: %.2f", value) load5Color -> formatString("L5: %.2f", value) load15Color -> formatString("L15: %.2f", value) - else -> formatString("Mem: %.0f MB", value) + else -> formatString("Mem: %s", freeMemoryChartLabel(value)) } }, ) @@ -233,7 +238,7 @@ internal fun HostMetricsChart( if (memData.isNotEmpty()) { VerticalAxis.rememberEnd( label = ChartStyling.rememberAxisLabel(color = memColor), - valueFormatter = { _, value, _ -> formatString("%.0f MB", value) }, + valueFormatter = { _, value, _ -> freeMemoryChartLabel(value) }, ) } else { null diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt index 1da33a78a5..916b83757e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt @@ -52,12 +52,13 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter +import org.meshtastic.core.common.util.formatByteSize import org.meshtastic.core.common.util.formatString import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.disk_free_indexed +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.free_memory import org.meshtastic.core.resources.host_metrics_log import org.meshtastic.core.resources.load_indexed @@ -155,27 +156,27 @@ private fun HostMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: /** Card body showing timestamp, load averages with progress bars, memory, disk, and uptime. */ @Composable -private fun HostMetricsCardContent(time: String, hostMetrics: org.meshtastic.proto.HostMetrics?) { +internal fun HostMetricsCardContent(time: String, hostMetrics: org.meshtastic.proto.HostMetrics?) { Column(modifier = Modifier.padding(12.dp)) { Text(text = time, style = MaterialTheme.typography.titleMediumEmphasized, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) hostMetrics?.uptime_seconds?.let { - LogLine(label = stringResource(Res.string.uptime), value = formatUptime(it)) + LogLine(label = stringResource(Res.string.uptime), value = formatDuration(it.toLong())) } hostMetrics?.freemem_bytes?.let { - LogLine(label = stringResource(Res.string.free_memory), value = formatBytes(it)) + LogLine(label = stringResource(Res.string.free_memory), value = formatByteSize(it)) } // Disk free rows hostMetrics?.diskfree1_bytes?.let { - LogLine(label = stringResource(Res.string.disk_free_indexed, 1), value = formatBytes(it)) + LogLine(label = stringResource(Res.string.disk_free_indexed, 1), value = formatByteSize(it)) } hostMetrics?.diskfree2_bytes?.let { - LogLine(label = stringResource(Res.string.disk_free_indexed, 2), value = formatBytes(it)) + LogLine(label = stringResource(Res.string.disk_free_indexed, 2), value = formatByteSize(it)) } hostMetrics?.diskfree3_bytes?.let { - LogLine(label = stringResource(Res.string.disk_free_indexed, 3), value = formatBytes(it)) + LogLine(label = stringResource(Res.string.disk_free_indexed, 3), value = formatByteSize(it)) } // Load averages with coloured indicators and progress bars @@ -229,36 +230,3 @@ fun LogLine(modifier: Modifier = Modifier, label: String, value: String) { Text(text = value) } } - -const val BYTES_IN_KB = 1024.0 -const val BYTES_IN_MB = BYTES_IN_KB * 1024.0 -const val BYTES_IN_GB = BYTES_IN_MB * 1024.0 - -private const val DECIMAL_FACTOR_1 = 10.0 -private const val DECIMAL_FACTOR_2 = 100.0 - -fun formatBytes(bytes: Long, decimalPlaces: Int = 2): String { - fun formatValue(value: Double): String { - // Simple decimal formatting without java.text.DecimalFormat - val factor = - when (decimalPlaces) { - 0 -> 1.0 - 1 -> DECIMAL_FACTOR_1 - else -> DECIMAL_FACTOR_2 - } - val rounded = kotlin.math.round(value * factor) / factor - return if (rounded == rounded.toLong().toDouble()) { - rounded.toLong().toString() - } else { - rounded.toString() - } - } - return when { - bytes < 0 -> "N/A" - bytes == 0L -> "0 B" - bytes >= BYTES_IN_GB -> "${formatValue(bytes / BYTES_IN_GB)} GB" - bytes >= BYTES_IN_MB -> "${formatValue(bytes / BYTES_IN_MB)} MB" - bytes >= BYTES_IN_KB -> "${formatValue(bytes / BYTES_IN_KB)} KB" - else -> "$bytes B" - } -} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 9b48d3bb73..7b8616fde6 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.TracerouteOverlay import org.meshtastic.core.model.evaluateTracerouteMapAvailability +import org.meshtastic.core.model.fixOrNull import org.meshtastic.core.model.noiseFloorOrNull import org.meshtastic.core.model.util.GeoConstants import org.meshtastic.core.model.util.TELEMETRY_CHANNEL_COUNT @@ -401,14 +402,8 @@ open class MetricsViewModel( header = "\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"\n", rows = data, epochSeconds = { it.time.toLong() }, - ) { pos -> - val lat = (pos.latitude_i ?: 0) * GeoConstants.DEG_D - val lon = (pos.longitude_i ?: 0) * GeoConstants.DEG_D - // Invariant: a CSV column is parsed, not read. A comma decimal here would shift every later field. - val heading = - NumberFormatter.formatInvariant((pos.ground_track ?: 0) * GeoConstants.HEADING_DEG, HEADING_DECIMALS) - "\"$lat\",\"$lon\",\"${pos.altitude}\",\"${pos.sats_in_view}\",\"${pos.ground_speed}\",\"$heading\"" - } + rowMapper = ::positionCsvRow, + ) } fun savePositionGpx(uri: CommonUri, data: List, trackName: String) { @@ -601,17 +596,33 @@ open class MetricsViewModel( protected fun decodeBase64(base64: String): ByteArray = base64.decodeBase64()?.toByteArray() ?: ByteArray(0) } +/** + * One position-log CSV row after the date and time columns. A field the position did not report is an empty cell, not + * `null` or 0. A CSV column is parsed, not read, so numbers are formatted invariantly. + */ +internal fun positionCsvRow(pos: org.meshtastic.proto.Position): String { + val lat = pos.latitude_i?.let { it * GeoConstants.DEG_D }?.toString().orEmpty() + val lon = pos.longitude_i?.let { it * GeoConstants.DEG_D }?.toString().orEmpty() + val altitude = pos.altitude?.toString().orEmpty() + val speed = pos.ground_speed?.toString().orEmpty() + val heading = + pos.ground_track?.let { NumberFormatter.formatInvariant(it * GeoConstants.HEADING_DEG, HEADING_DECIMALS) } + return "\"$lat\",\"$lon\",\"$altitude\",\"${pos.sats_in_view}\",\"$speed\",\"${heading.orEmpty()}\"" +} + /** * Coordinates are formatted invariantly: GPX is XML another program parses, and a comma decimal makes * `lat="52,5200000"` — a file no importer accepts. Internal so a test can pin a comma locale and prove it. */ internal fun buildGpx(positions: List, trackName: String): String { val trkpts = buildString { - for (pos in positions) { - val lat = NumberFormatter.formatInvariant((pos.latitude_i ?: 0) * GeoConstants.DEG_D, COORDINATE_DECIMALS) - val lon = NumberFormatter.formatInvariant((pos.longitude_i ?: 0) * GeoConstants.DEG_D, COORDINATE_DECIMALS) + // Track points are joined in file order, so oldest first, as the track map draws them. + for (pos in positions.sortedBy { it.time }) { + val (latI, lonI) = pos.fixOrNull() ?: continue + val lat = NumberFormatter.formatInvariant(latI * GeoConstants.DEG_D, COORDINATE_DECIMALS) + val lon = NumberFormatter.formatInvariant(lonI * GeoConstants.DEG_D, COORDINATE_DECIMALS) append(" ") - if ((pos.altitude ?: 0) != 0) append("${pos.altitude}") + pos.altitude?.let { append("$it") } if (pos.time > 0) append("") append("\n") } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt index d88c19e03a..d650118b79 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt @@ -52,9 +52,9 @@ import org.meshtastic.core.common.util.formatString import org.meshtastic.core.model.MeshLog import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ble_devices +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.no_pax_metrics_logs import org.meshtastic.core.resources.pax import org.meshtastic.core.resources.pax_ble_format @@ -66,9 +66,6 @@ import org.meshtastic.core.resources.pax_wifi_format import org.meshtastic.core.resources.pax_wifi_marker import org.meshtastic.core.resources.uptime import org.meshtastic.core.resources.wifi_devices -import org.meshtastic.core.ui.component.IconInfo -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.PeopleCount import org.meshtastic.core.ui.theme.GraphColors.Orange import org.meshtastic.core.ui.theme.GraphColors.Purple import org.meshtastic.proto.Paxcount as ProtoPaxcount @@ -249,21 +246,6 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Uni ) } -@Composable -fun PaxcountInfo( - pax: String, - modifier: Modifier = Modifier, - contentColor: Color = MaterialTheme.colorScheme.onSurface, -) { - IconInfo( - modifier = modifier, - icon = MeshtasticIcons.PeopleCount, - contentDescription = stringResource(Res.string.pax_metrics_log), - text = pax, - contentColor = contentColor, - ) -} - @Composable fun PaxMetricsItem(log: MeshLog, pax: ProtoPaxcount, isSelected: Boolean, onClick: () -> Unit) { SelectableMetricCard(isSelected = isSelected, onClick = onClick) { @@ -297,7 +279,7 @@ fun PaxMetricsItem(log: MeshLog, pax: ProtoPaxcount, isSelected: Boolean, onClic } Text( - text = stringResource(Res.string.uptime) + ": " + formatUptime(pax.uptime), + text = stringResource(Res.string.uptime) + ": " + formatDuration(pax.uptime.toLong()), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.End, ) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt index ee37b6d576..4cb81cfac3 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt @@ -36,6 +36,7 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.MeasurementSystem import org.meshtastic.core.common.util.formatString +import org.meshtastic.core.model.fixOrNull import org.meshtastic.core.model.util.GeoConstants.DEG_D import org.meshtastic.core.model.util.GeoConstants.HEADING_DEG import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC @@ -61,8 +62,7 @@ import org.meshtastic.proto.Position @Suppress("LongMethod") fun PositionCard(position: Position, displayUnits: MeasurementSystem, isSelected: Boolean, onClick: () -> Unit) { val time = position.time.toLong() * MS_PER_SEC - val latitude = formatString("%.5f", (position.latitude_i ?: 0) * DEG_D) - val longitude = formatString("%.5f", (position.longitude_i ?: 0) * DEG_D) + val fix = position.fixOrNull() SelectableMetricCard(isSelected = isSelected, onClick = onClick) { Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) { @@ -84,13 +84,20 @@ fun PositionCard(position: Position, displayUnits: MeasurementSystem, isSelected verticalArrangement = Arrangement.spacedBy(4.dp), itemVerticalAlignment = Alignment.CenterVertically, ) { - Row(verticalAlignment = Alignment.CenterVertically) { - MetricValueRow(color = GraphColors.Blue, text = "${stringResource(Res.string.latitude)}: $latitude") - Spacer(Modifier.width(12.dp)) - MetricValueRow( - color = GraphColors.Green, - text = "${stringResource(Res.string.longitude)}: $longitude", - ) + if (fix != null) { + val (latitudeI, longitudeI) = fix + Row(verticalAlignment = Alignment.CenterVertically) { + MetricValueRow( + color = GraphColors.Blue, + text = "${stringResource(Res.string.latitude)}: ${formatString("%.5f", latitudeI * DEG_D)}", + ) + Spacer(Modifier.width(12.dp)) + MetricValueRow( + color = GraphColors.Green, + text = + "${stringResource(Res.string.longitude)}: ${formatString("%.5f", longitudeI * DEG_D)}", + ) + } } Text( text = "${stringResource(Res.string.sats)}: ${position.sats_in_view}", diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt index 66248d19d9..89d90b6d48 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt @@ -24,10 +24,12 @@ import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.hasFix import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.clear import org.meshtastic.core.resources.clear_position_track_message @@ -51,6 +53,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val exportPositionLauncher = rememberSaveFileLauncher { uri -> viewModel.savePositionCSV(uri, positions) } val nodeName = state.node?.user?.long_name ?: "" val exportGpxLauncher = rememberSaveFileLauncher { uri -> viewModel.savePositionGpx(uri, positions, nodeName) } + val hasTrackPoint = remember(positions) { positions.any { it.hasFix() } } val trackMap = LocalNodeTrackMapProvider.current val destNum = state.node?.num ?: 0 @@ -64,13 +67,15 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { timeProvider = { it.time.toDouble() }, onExportCsv = { exportPositionLauncher("position.csv", "text/csv") }, extraActions = { - if (positions.isNotEmpty()) { + if (hasTrackPoint) { IconButton(onClick = { exportGpxLauncher("track.gpx", "application/gpx+xml") }) { Icon( imageVector = MeshtasticIcons.FileDownload, contentDescription = stringResource(Res.string.export_gpx), ) } + } + if (positions.isNotEmpty()) { ClearPositionTrackButton(onConfirm = { viewModel.clearPosition() }) } if (!state.isLocal) { @@ -81,7 +86,9 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { }, chartPart = { modifier, selectedX, _, onPointSelected -> val selectedTime = selectedX?.toInt() - trackMap(destNum, positions, modifier, selectedTime) { time -> onPointSelected(time.toDouble()) } + // Positional: trackMap is a function type, so it takes no named arguments. Collapsed credit, this being a + // strip inside a screen whose own map is a tab away. + trackMap(destNum, positions, modifier, selectedTime, { time -> onPointSelected(time.toDouble()) }, false) }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt index 8ea59002ba..3020ab7681 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt @@ -274,18 +274,18 @@ private fun PowerMetricsChart( modelProducer.runTransaction { if (currentData.isNotEmpty()) { lineModel { - series( - x = currentData.map { it.time }, - y = currentData.map { retrieveCurrent(selectedChannel, it) }, - ) + splitAtGaps(currentData) { it.time } + .forEach { run -> + series(x = run.map { it.time }, y = run.map { retrieveCurrent(selectedChannel, it) }) + } } } if (voltageData.isNotEmpty()) { lineModel { - series( - x = voltageData.map { it.time }, - y = voltageData.map { retrieveVoltage(selectedChannel, it) }, - ) + splitAtGaps(voltageData) { it.time } + .forEach { run -> + series(x = run.map { it.time }, y = run.map { retrieveVoltage(selectedChannel, it) }) + } } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt index 8376d02bf9..1e6b0c4cfc 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -58,12 +59,12 @@ import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.noiseFloorOrNull import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.model.util.rxTimeOrNull import org.meshtastic.core.model.util.snrOrNull import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.busy_noise_floor import org.meshtastic.core.resources.clear +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.local_stats_bad import org.meshtastic.core.resources.local_stats_nodes import org.meshtastic.core.resources.local_stats_noise @@ -460,6 +461,7 @@ private fun SignalMetricsChart( } @Composable +@ReadOnlyComposable private fun noiseFloorTextColor(value: Int?): Color = when { value == null -> MaterialTheme.colorScheme.onSurfaceVariant value < QUIET_NOISE_FLOOR_DBM -> SignalMetric.SNR.color @@ -547,7 +549,11 @@ private fun LocalStatsCard(telemetry: Telemetry, isSelected: Boolean, onClick: ( style = MaterialTheme.typography.labelLarge, ) Text( - text = stringResource(Res.string.local_stats_uptime, formatUptime(localStats?.uptime_seconds ?: 0)), + text = + stringResource( + Res.string.local_stats_uptime, + formatDuration((localStats?.uptime_seconds ?: 0).toLong()), + ), style = MaterialTheme.typography.labelLarge, ) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteChart.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteChart.kt index fe5022f087..bb67c5fa97 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteChart.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteChart.kt @@ -158,14 +158,18 @@ internal fun TracerouteMetricsChart( modelProducer.runTransaction { if (forwardData.isNotEmpty()) { lineModel { - series(x = forwardData.map { it.timeSeconds }, y = forwardData.map { it.forwardHops!! }) + series(x = forwardData.map { it.timeSeconds }, y = forwardData.mapNotNull { it.forwardHops }) } } if (returnData.isNotEmpty()) { - lineModel { series(x = returnData.map { it.timeSeconds }, y = returnData.map { it.returnHops!! }) } + lineModel { + series(x = returnData.map { it.timeSeconds }, y = returnData.mapNotNull { it.returnHops }) + } } if (rttData.isNotEmpty()) { - lineModel { series(x = rttData.map { it.timeSeconds }, y = rttData.map { it.roundTripSeconds!! }) } + lineModel { + series(x = rttData.map { it.timeSeconds }, y = rttData.mapNotNull { it.roundTripSeconds }) + } } } } diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt similarity index 100% rename from feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt index 500883b0ea..7f65007b18 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt @@ -17,9 +17,9 @@ package org.meshtastic.feature.node.model import org.meshtastic.core.common.util.MeasurementSystem -import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.DeviceLink +import org.meshtastic.core.model.FirmwareRelease import org.meshtastic.core.model.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.rxTimeOrNull diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt index 6d5a618794..ee312702b4 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt @@ -69,6 +69,7 @@ import org.meshtastic.feature.node.metrics.PositionLogScreen import org.meshtastic.feature.node.metrics.PowerMetricsScreen import org.meshtastic.feature.node.metrics.SignalMetricsScreen import org.meshtastic.feature.node.metrics.TracerouteLogScreen +import org.meshtastic.feature.node.metrics.TracerouteMapScreen import kotlin.reflect.KClass @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -128,12 +129,14 @@ fun EntryProviderScope.nodeDetailGraph(backStack: NavBackStack) } entry(metadata = { ListDetailSceneStrategy.extraPane() }) { args -> - val tracerouteMapScreen = org.meshtastic.core.ui.util.LocalTracerouteMapScreenProvider.current - tracerouteMapScreen( - args.destNum, - args.requestId, - args.logUuid, - dropUnlessResumed { backStack.removeLastOrNull() }, + val metricsViewModel = koinViewModel { parametersOf(args.destNum) } + metricsViewModel.setNodeId(args.destNum) + + TracerouteMapScreen( + metricsViewModel = metricsViewModel, + requestId = args.requestId, + logUuid = args.logUuid, + onNavigateUp = dropUnlessResumed { backStack.removeLastOrNull() }, ) } @@ -188,7 +191,6 @@ private inline fun EntryProviderScope.addNodeDetailS } } -/** Expect declaration for the platform-specific traceroute map screen. */ enum class NodeDetailScreen( val title: StringResource, val routeClass: KClass, diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt index a3ce85c8ab..71f6dcbb76 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt @@ -19,6 +19,7 @@ package org.meshtastic.feature.node.list import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.ExcludedModules import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -50,11 +51,25 @@ class StatusMessageActionGateTest { @Test fun `older firmware does not offer the action`() { - val old = node(num = 1, firmware = "2.7.21") + val old = node(num = 1, firmware = "2.7.19") assertFalse(canEditStatusMessage(old, old, ConnectionState.Connected)) } + @Test + fun `firmware that compiled the module out does not offer the action`() { + val metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.8.0" + wb.excluded_modules = ExcludedModules.STATUSMESSAGE_CONFIG.value + } + .build() + val slim = Node(num = 1, metadata = metadata) + + assertFalse(canEditStatusMessage(slim, slim, ConnectionState.Connected)) + } + @Test fun `firmware metadata we have not read yet does not offer the action`() { val unknown = node(num = 1, firmware = null) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/ChartGapsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/ChartGapsTest.kt new file mode 100644 index 0000000000..131967bcc0 --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/ChartGapsTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChartGapsTest { + private fun split(vararg times: Int) = splitAtGaps(times.toList()) { it } + + @Test + fun emptyAndSingleInputsYieldNoGapSplit() { + assertEquals(emptyList(), split()) + assertEquals(listOf(listOf(100)), split(100)) + } + + @Test + fun evenlySpacedReadingsStayOneRun() { + assertEquals(listOf(listOf(0, 900, 1800, 2700)), split(0, 900, 1800, 2700)) + } + + @Test + fun longSilenceBreaksTheRun() { + assertEquals( + listOf(listOf(0, 900, 1800), listOf(30_000, 30_900)), + split(0, 900, 1800, 30_000, 30_900), + ) + } + + @Test + fun thresholdScalesWithMedianSpacing() { + assertEquals(listOf(listOf(0, 1800, 3600, 9000)), split(0, 1800, 3600, 9000)) + assertEquals(listOf(listOf(0, 1800, 3600), listOf(9100)), split(0, 1800, 3600, 9100)) + } + + @Test + fun evenSpacingCountUsesTheTrueMedian() { + assertEquals(listOf(listOf(0, 60, 120, 720), listOf(2220)), split(0, 60, 120, 720, 2220)) + } + + @Test + fun denseReadingsUseTheFiveMinuteFloor() { + assertEquals(listOf(listOf(0, 30, 60, 360)), split(0, 30, 60, 360)) + assertEquals(listOf(listOf(0, 30, 60), listOf(361)), split(0, 30, 60, 361)) + } + + @Test + fun isolatedReadingBetweenGapsIsItsOwnRun() { + assertEquals( + listOf(listOf(0, 60, 120), listOf(10_000), listOf(20_000, 20_060)), + split(0, 60, 120, 10_000, 20_000, 20_060), + ) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt index 7d94e41e98..a5dfd54788 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt @@ -196,7 +196,7 @@ class DecodePaxFromLogTest { * This avoids needing to instantiate the full ViewModel with all its dependencies. The logic is identical to the * ViewModel method. */ -@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount") +@Suppress("CyclomaticComplexMethod", "ReturnCount") private fun decodePaxFromLogStandalone(log: MeshLog): ProtoPaxcount? { try { val packet = log.fromRadio.packet diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt index 753e49887a..0ffa621ba9 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt @@ -23,7 +23,6 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -@Suppress("MagicNumber") class EnvironmentChartUnitsTest { private fun telemetry(env: EnvironmentMetrics) = Telemetry.Builder() diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt index ac354fa1a0..6ead87db6d 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt @@ -24,7 +24,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("MagicNumber") class EnvironmentMetricsForGraphingTest { private val now = nowSeconds.toInt() diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/FormatBytesTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/FormatBytesTest.kt deleted file mode 100644 index 397b745c6b..0000000000 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/FormatBytesTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals - -/** Tests for [formatBytes] — the pure function that formats byte counts into human-readable strings. */ -@Suppress("MagicNumber") -class FormatBytesTest { - - @Test - fun zero_bytes() { - assertEquals("0 B", formatBytes(0L)) - } - - @Test - fun small_byte_values() { - assertEquals("1 B", formatBytes(1L)) - assertEquals("512 B", formatBytes(512L)) - assertEquals("1023 B", formatBytes(1023L)) - } - - @Test - fun kilobyte_boundary() { - assertEquals("1 KB", formatBytes(1024L)) - } - - @Test - fun kilobyte_with_decimals() { - // 1536 bytes = 1.5 KB - assertEquals("1.5 KB", formatBytes(1536L)) - } - - @Test - fun kilobytes_just_below_megabyte_boundary_round_up_without_switching_units() { - assertEquals("1024 KB", formatBytes(1_048_575L)) - } - - @Test - fun megabyte_boundary() { - assertEquals("1 MB", formatBytes(1024L * 1024)) - } - - @Test - fun megabyte_with_decimals() { - // 1.5 MB = 1572864 bytes - assertEquals("1.5 MB", formatBytes(1_572_864L)) - } - - @Test - fun gigabyte_boundary() { - assertEquals("1 GB", formatBytes(1024L * 1024 * 1024)) - } - - @Test - fun gigabyte_with_decimals() { - // 2.5 GB - assertEquals("2.5 GB", formatBytes((2.5 * 1024 * 1024 * 1024).toLong())) - } - - @Test - fun negative_bytes_returns_na() { - assertEquals("N/A", formatBytes(-1L)) - assertEquals("N/A", formatBytes(-1024L)) - } - - @Test - fun large_values() { - // 100 GB - assertEquals("100 GB", formatBytes(100L * 1024 * 1024 * 1024)) - } - - @Test - fun custom_decimal_places_zero() { - // 1536 bytes = 1.5 KB, with 0 decimal places → 2 KB (rounded) - assertEquals("2 KB", formatBytes(1536L, decimalPlaces = 0)) - } - - @Test - fun custom_decimal_places_one() { - // 1536 bytes = 1.5 KB, with 1 decimal place → 1.5 KB - assertEquals("1.5 KB", formatBytes(1536L, decimalPlaces = 1)) - } - - @Test - fun default_rounding_keeps_two_decimal_places_without_trailing_zeroes() { - assertEquals("1.46 KB", formatBytes(1500L)) - assertEquals("1.5 KB", formatBytes(1536L)) - } -} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxAltitudeTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxAltitudeTest.kt new file mode 100644 index 0000000000..848ea446fa --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxAltitudeTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import org.meshtastic.proto.Position +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class GpxAltitudeTest { + + private fun gpxFor(altitude: Int?): String = buildGpx( + listOf( + Position.Builder() + .also { wb -> + wb.latitude_i = 525_200_000 + wb.longitude_i = 134_050_000 + wb.altitude = altitude + } + .build(), + ), + "track", + ) + + @Test + fun `a sea level point keeps its elevation`() { + assertTrue(gpxFor(altitude = 0).contains("0")) + } + + @Test + fun `a point without altitude has no elevation element`() { + assertFalse(gpxFor(altitude = null).contains("")) + } + + @Test + fun `a measured altitude is written as the elevation`() { + assertTrue(gpxFor(altitude = 34).contains("34")) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxTrackPointsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxTrackPointsTest.kt new file mode 100644 index 0000000000..3bd4821214 --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxTrackPointsTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import org.meshtastic.proto.Position +import kotlin.test.Test +import kotlin.test.assertEquals + +class GpxTrackPointsTest { + + private fun position(latitude: Int?, longitude: Int?, time: Int = 0) = Position.Builder() + .also { wb -> + wb.latitude_i = latitude + wb.longitude_i = longitude + wb.time = time + } + .build() + + private val berlin = position(latitude = 525_200_000, longitude = 134_050_000) + + /** The `lat` and `lon` attributes of every track point, in file order. */ + private fun trackPoints(vararg positions: Position): List> = + Regex("""""") + .findAll(buildGpx(positions.toList(), "track")) + .map { it.groupValues[1] to it.groupValues[2] } + .toList() + + @Test + fun `a position without latitude is left out of the track`() { + assertEquals( + listOf("52.5200000" to "13.4050000"), + trackPoints(position(latitude = null, longitude = 134_050_000), berlin), + ) + } + + @Test + fun `a position without longitude is left out of the track`() { + assertEquals( + listOf("52.5200000" to "13.4050000"), + trackPoints(position(latitude = 525_200_000, longitude = null), berlin), + ) + } + + @Test + fun `a position at zero latitude and zero longitude is left out of the track`() { + assertEquals(listOf("52.5200000" to "13.4050000"), trackPoints(position(latitude = 0, longitude = 0), berlin)) + } + + @Test + fun `points on the equator and the prime meridian are kept`() { + assertEquals( + listOf("0.0000000" to "13.4050000", "52.5200000" to "0.0000000"), + trackPoints( + position(latitude = 0, longitude = 134_050_000), + position(latitude = 525_200_000, longitude = 0), + ), + ) + } + + @Test + fun `points are written oldest first`() { + val newer = position(latitude = 525_200_000, longitude = 134_050_000, time = 1_700_000_600) + val older = position(latitude = 481_370_000, longitude = 115_750_000, time = 1_700_000_000) + + assertEquals(listOf("48.1370000" to "11.5750000", "52.5200000" to "13.4050000"), trackPoints(newer, older)) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt index 82b34dbc99..be945087f4 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt @@ -23,7 +23,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("MagicNumber") class HostMetricsTest { private fun telemetry(time: Int, hostMetrics: HostMetrics? = null) = Telemetry.Builder() @@ -46,7 +45,7 @@ class HostMetricsTest { wb.load1 = 150 wb.load5 = 0 wb.load15 = 225 - wb.freemem_bytes = 2_097_152L + wb.freemem_bytes = 2_000_000L } .build(), ), @@ -100,4 +99,22 @@ class HostMetricsTest { assertTrue(chartData.load15.isEmpty()) assertTrue(chartData.freeMemoryMb.isEmpty()) } + + @Test + fun free_memory_plots_and_labels_in_decimal_megabytes() { + val sixteenGib = 17_179_869_184L + val chartData = + buildHostMetricsChartData( + listOf( + telemetry( + time = 100, + hostMetrics = HostMetrics.Builder().also { wb -> wb.freemem_bytes = sixteenGib }.build(), + ), + ), + ) + + val point = chartData.freeMemoryMb.single() + assertEquals(17_179.869184, point.value) + assertEquals("17,180 MB", freeMemoryChartLabel(point.value)) + } } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PositionCsvRowTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PositionCsvRowTest.kt new file mode 100644 index 0000000000..74fb325100 --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PositionCsvRowTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import org.meshtastic.proto.Position +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class PositionCsvRowTest { + + private fun row( + altitude: Int?, + speed: Int?, + track: Int?, + latitude: Int? = 525_200_000, + longitude: Int? = 134_050_000, + ) = positionCsvRow( + Position.Builder() + .also { wb -> + wb.latitude_i = latitude + wb.longitude_i = longitude + wb.sats_in_view = 5 + wb.altitude = altitude + wb.ground_speed = speed + wb.ground_track = track + } + .build(), + ) + + private fun String.cells() = removeSurrounding("\"").split("\",\"") + + /** Altitude, satellites, speed and heading, the cells after latitude and longitude. */ + private fun String.trailingCells() = cells().drop(2) + + /** Latitude and longitude. */ + private fun String.coordinateCells() = cells().take(2) + + @Test + fun `fields the position did not report are empty cells`() { + val line = row(altitude = null, speed = null, track = null) + + assertFalse(line.contains("null"), line) + assertEquals(listOf("", "5", "", ""), line.trailingCells()) + } + + @Test + fun `zero readings are written as zero`() { + assertEquals(listOf("0", "5", "0", "0.00"), row(altitude = 0, speed = 0, track = 0).trailingCells()) + } + + @Test + fun `coordinates the position did not report are empty cells`() { + val line = row(altitude = 0, speed = 0, track = 0, latitude = null, longitude = null) + + assertEquals(listOf("", ""), line.coordinateCells()) + } + + @Test + fun `zero coordinates are written as zero`() { + val line = row(altitude = 0, speed = 0, track = 0, latitude = 0, longitude = 0) + + assertEquals(listOf("0.0", "0.0"), line.coordinateCells()) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt index b25e840bd4..516b9f8bd2 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt @@ -37,7 +37,6 @@ import kotlin.test.assertNull * [MeshPacket.fullRouteDiscovery] prepends the destination and appends the source to produce the full route. For * `route_back` to be wrapped with endpoints, `hop_start > 0` and `snr_back` must be non-empty. */ -@Suppress("MagicNumber") class TracerouteChartTest { companion object { diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/TimeFrameTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/TimeFrameTest.kt index 87579610d3..7c95df29ca 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/TimeFrameTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/TimeFrameTest.kt @@ -22,7 +22,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("MagicNumber") class TimeFrameTest { // ---- timeThreshold ---- diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsCardBytesTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsCardBytesTest.kt new file mode 100644 index 0000000000..512ea28bb6 --- /dev/null +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsCardBytesTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.proto.HostMetrics +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class HostMetricsCardBytesTest { + + @Test + fun `free memory and disk rows read in decimal units`() = runComposeUiTest { + val hostMetrics = + HostMetrics.Builder() + .also { wb -> + wb.freemem_bytes = 17_179_869_184L + wb.diskfree1_bytes = 1_500_000_000L + } + .build() + setContent { MaterialTheme { HostMetricsCardContent(time = "", hostMetrics = hostMetrics) } } + + onNodeWithText("17.18 GB").assertExists() + onNodeWithText("1.50 GB").assertExists() + onNodeWithText("16 GB").assertDoesNotExist() + } +} diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/PositionCardCoordinatesTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/PositionCardCoordinatesTest.kt new file mode 100644 index 0000000000..1b4729e984 --- /dev/null +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/PositionCardCoordinatesTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.metrics + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.common.util.formatString +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.latitude +import org.meshtastic.core.resources.longitude +import org.meshtastic.proto.Position +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class PositionCardCoordinatesTest { + + @Test + fun `a position without latitude shows no coordinates`() = runComposeUiTest { + setPositionCard(latitude = null, longitude = 134_050_000) + + assertNoCoordinates() + } + + @Test + fun `a position without longitude shows no coordinates`() = runComposeUiTest { + setPositionCard(latitude = 525_200_000, longitude = null) + + assertNoCoordinates() + } + + @Test + fun `a position at zero latitude and zero longitude shows no coordinates`() = runComposeUiTest { + setPositionCard(latitude = 0, longitude = 0) + + assertNoCoordinates() + } + + @Test + fun `a position on the equator shows both coordinates`() = runComposeUiTest { + setPositionCard(latitude = 0, longitude = 134_050_000) + + onNodeWithText(coordinate(Res.string.latitude, 0.0), useUnmergedTree = true).assertExists() + onNodeWithText(coordinate(Res.string.longitude, 13.405), useUnmergedTree = true).assertExists() + } + + private fun ComposeUiTest.assertNoCoordinates() { + onNodeWithText(getString(Res.string.latitude), substring = true, useUnmergedTree = true).assertDoesNotExist() + onNodeWithText(getString(Res.string.longitude), substring = true, useUnmergedTree = true).assertDoesNotExist() + } + + private fun coordinate(label: StringResource, degrees: Double) = + "${getString(label)}: ${formatString("%.5f", degrees)}" + + private fun ComposeUiTest.setPositionCard(latitude: Int?, longitude: Int?) { + val position = + Position.Builder() + .also { wb -> + wb.latitude_i = latitude + wb.longitude_i = longitude + wb.time = 1_700_000_000 + } + .build() + setContent { + MaterialTheme { + PositionCard( + position = position, + displayUnits = MeasurementSystem.METRIC, + isSelected = false, + onClick = {}, + ) + } + } + } +} diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/navigation/TracerouteMapRouteTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/navigation/TracerouteMapRouteTest.kt new file mode 100644 index 0000000000..e175bec006 --- /dev/null +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/navigation/TracerouteMapRouteTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.navigation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.koin.compose.KoinIsolatedContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.koinApplication +import org.koin.dsl.module +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.Node +import org.meshtastic.core.navigation.NodeDetailRoute +import org.meshtastic.core.repository.FileService +import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.TracerouteResponseProvider +import org.meshtastic.core.repository.TracerouteSnapshotRepository +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.traceroute_showing_nodes +import org.meshtastic.core.ui.component.MeshtasticNavDisplay +import org.meshtastic.core.ui.util.AlertManager +import org.meshtastic.core.ui.util.LocalTracerouteMapProvider +import org.meshtastic.feature.node.detail.NodeDetailUiState +import org.meshtastic.feature.node.detail.NodeRequestActions +import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase +import org.meshtastic.feature.node.metrics.MetricsViewModel +import org.meshtastic.feature.node.model.MetricsState +import org.meshtastic.proto.User +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) +class TracerouteMapRouteTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun tracerouteMapRouteRendersTheMapScreenAroundTheHostMap() = runComposeUiTest { + val route = NodeDetailRoute.TracerouteMap(destNum = NODE_NUM, requestId = REQUEST_ID) + var factoryDestNum: Int? = null + val koinApp = koinApplication { + modules( + module { + viewModel { params -> + createMetricsViewModel(destNum = params.get().also { factoryDestNum = it }) + } + }, + ) + } + val viewModelStoreOwner = TestViewModelStoreOwner() + val lifecycleOwner = ResumedLifecycleOwner() + try { + setContent { + val backStack = remember { NavBackStack().apply { add(route) } } + KoinIsolatedContext(context = koinApp) { + CompositionLocalProvider( + LocalViewModelStoreOwner provides viewModelStoreOwner, + LocalLifecycleOwner provides lifecycleOwner, + LocalTracerouteMapProvider provides + { _, _, onMappableCountChanged, modifier -> + LaunchedEffect(Unit) { onMappableCountChanged(SHOWN_NODES, TOTAL_NODES) } + Text(text = FAKE_MAP, modifier = modifier) + }, + ) { + MaterialTheme { + MeshtasticNavDisplay( + backStack = backStack, + entryProvider = entryProvider { nodeDetailGraph(backStack) }, + ) + } + } + } + } + + onNodeWithText(PLACEHOLDER).assertDoesNotExist() + onNodeWithText(FAKE_MAP).assertExists() + onNodeWithText(NODE_NAME).assertExists() + onNodeWithText(getString(Res.string.traceroute_showing_nodes, SHOWN_NODES, TOTAL_NODES)).assertExists() + assertEquals(NODE_NUM, factoryDestNum) + } finally { + viewModelStoreOwner.viewModelStore.clear() + koinApp.close() + } + } + + private fun createMetricsViewModel(destNum: Int): MetricsViewModel { + val tracerouteResponseProvider: TracerouteResponseProvider = mock() + every { tracerouteResponseProvider.tracerouteResponse } returns MutableStateFlow(null) + every { tracerouteResponseProvider.clearTracerouteResponse() } returns Unit + val nodeRequestActions: NodeRequestActions = mock() + every { nodeRequestActions.lastTracerouteTime } returns MutableStateFlow(null) + every { nodeRequestActions.lastRequestNeighborTimes } returns MutableStateFlow(emptyMap()) + val nodeRepository: NodeRepository = mock() + every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) + val node = Node(num = NODE_NUM, user = User.Builder().also { wb -> wb.long_name = NODE_NAME }.build()) + val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock() + every { getNodeDetailsUseCase(NODE_NUM) } returns + flowOf(NodeDetailUiState(node = node, metricsState = MetricsState(node = node))) + + return MetricsViewModel( + destNum = destNum, + dispatchers = CoroutineDispatchers(io = testDispatcher, main = testDispatcher, default = testDispatcher), + meshLogRepository = mock(), + tracerouteResponseProvider = tracerouteResponseProvider, + nodeRepository = nodeRepository, + tracerouteSnapshotRepository = mock(), + nodeRequestActions = nodeRequestActions, + alertManager = mock(), + getNodeDetailsUseCase = getNodeDetailsUseCase, + fileService = mock(), + ) + } + + private class TestViewModelStoreOwner : ViewModelStoreOwner { + override val viewModelStore = ViewModelStore() + } + + private class ResumedLifecycleOwner : LifecycleOwner { + override val lifecycle: LifecycleRegistry = + LifecycleRegistry.createUnsafe(this).apply { currentState = Lifecycle.State.RESUMED } + } + + private companion object { + const val NODE_NUM = 1234 + const val NODE_NAME = "Route target" + const val REQUEST_ID = 42 + const val SHOWN_NODES = 2 + const val TOTAL_NODES = 3 + const val FAKE_MAP = "host traceroute map" + const val PLACEHOLDER = "Traceroute Map" + } +} diff --git a/feature/settings/README.md b/feature/settings/README.md index 211762db3a..0ec98bb5f3 100644 --- a/feature/settings/README.md +++ b/feature/settings/README.md @@ -32,18 +32,13 @@ Displays the open-source libraries the app is built on, with their licenses. ```mermaid graph TB :feature:settings[settings]:::kmp-feature - :feature:settings -.-> :core:barcode - :feature:settings -.-> :core:nfc :feature:settings -.-> :core:common - :feature:settings -.-> :core:data :feature:settings -.-> :core:database - :feature:settings -.-> :core:datastore :feature:settings -.-> :core:domain :feature:settings -.-> :core:model :feature:settings -.-> :core:navigation :feature:settings -.-> :core:network :feature:settings -.-> :core:repository - :feature:settings -.-> :core:service :feature:settings -.-> :core:resources :feature:settings -.-> :core:ui :feature:settings -.-> :core:di diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 23d9f73631..1c9827791c 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -22,19 +22,17 @@ plugins { } kotlin { + // No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT. sourceSets { commonMain.dependencies { implementation(projects.core.common) - implementation(projects.core.data) implementation(projects.core.database) - implementation(projects.core.datastore) implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.navigation) implementation(projects.core.network) implementation(libs.meshtastic.protobufs) implementation(projects.core.repository) - implementation(projects.core.service) implementation(projects.core.resources) implementation(projects.core.ui) implementation(projects.core.di) @@ -46,14 +44,9 @@ kotlin { implementation(libs.coil) } - androidMain.dependencies { - implementation(projects.core.barcode) - implementation(projects.core.nfc) - implementation(libs.androidx.appcompat) - } + androidMain.dependencies { implementation(libs.androidx.appcompat) } commonTest.dependencies { - implementation(projects.core.datastore) implementation(projects.core.testing) implementation(libs.compose.multiplatform.ui.test) } diff --git a/feature/settings/detekt-baseline.xml b/feature/settings/detekt-baseline.xml index 25edb0f49d..b4b30ff8ae 100644 --- a/feature/settings/detekt-baseline.xml +++ b/feature/settings/detekt-baseline.xml @@ -12,24 +12,17 @@ ComposableParamOrder:WarningDialog.kt:@Composable fun WarningDialog CyclomaticComplexMethod:RadioConfigViewModel.kt:RadioConfigViewModel$private fun processPacketResponse LambdaParameterEventTrailing:NodeActionButton.kt:onClick: () -> Unit - LambdaParameterInRestartableEffect:PacketResponseStateDialog.kt:onBack: () -> Unit = {} - LambdaParameterInRestartableEffect:PacketResponseStateDialog.kt:onDismiss: () -> Unit = {} - LambdaParameterInRestartableEffect:PrivacySection.kt:startProvideLocation: () -> Unit - LambdaParameterInRestartableEffect:PrivacySection.kt:stopProvideLocation: () -> Unit - LambdaParameterInRestartableEffect:TakPermissionUtil.kt:onPermissionResult: (Boolean) -> Unit LongMethod:AudioConfigItemList.kt:@Composable fun AudioConfigScreen LongMethod:DetectionSensorConfigItemList.kt:@Composable fun DetectionSensorConfigScreen LongMethod:LoRaConfigItemList.kt:@Composable fun LoRaConfigScreen LongMethod:PowerConfigItemList.kt:@Composable fun PowerConfigScreen LongMethod:RadioConfigViewModel.kt:RadioConfigViewModel$private fun processPacketResponse - LongMethod:SecurityConfigScreen.android.kt:@Composable actual fun SecurityKeyBackupActions LongMethod:SerialConfigItemList.kt:@Composable fun SerialConfigScreen LongMethod:StoreForwardConfigItemList.kt:@Composable fun StoreForwardConfigScreen LongMethod:TelemetryConfigItemList.kt:@Composable fun TelemetryConfigScreen LongMethod:UserConfigItemList.kt:@Composable fun UserConfigScreen MagicNumber:Debug.kt:3 MagicNumber:DebugViewModel.kt:DebugViewModel$16 - MagicNumber:DebugViewModel.kt:DebugViewModel$8 MagicNumber:EditChannelDialog.kt:16 MagicNumber:EditChannelDialog.kt:32 MagicNumber:EditDeviceProfileDialog.kt:ProfileField.CHANNEL_URL$3 @@ -61,7 +54,6 @@ ModifierMissing:NeighborInfoConfigItemList.kt:@Composable fun NeighborInfoConfigScreen ModifierMissing:NetworkConfigItemList.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun NetworkConfigScreen ModifierMissing:PacketResponseStateDialog.kt:@Composable fun <T> PacketResponseStateDialog - ModifierMissing:PaxcounterConfigItemList.kt:@Composable fun PaxcounterConfigScreen ModifierMissing:PositionConfigScreen.android.kt:@Composable actual fun DeviceLocationButton ModifierMissing:PositionConfigScreen.kt:@Composable @Suppress("LongMethod", "CyclomaticComplexMethod") fun PositionConfigScreenCommon ModifierMissing:PowerConfigItemList.kt:@Composable fun PowerConfigScreen @@ -80,7 +72,12 @@ MultipleEmitters:PacketResponseStateDialog.kt:@Composable private fun ErrorContent MultipleEmitters:PacketResponseStateDialog.kt:@Composable private fun SuccessContent MultipleEmitters:SecurityConfigScreen.android.kt:@Composable actual fun SecurityKeyBackupActions - MutableStateAutoboxing:DesktopSettingsScreen.kt:mutableStateOf(0) + NoNameShadowing:Debug.kt:{ filterMode = it } + NoNameShadowing:DebugViewModel.kt:LogSearchManager${ SearchMatch(logIndex, it.range.first, it.range.last, "decodedPayload") } + NoNameShadowing:MeshBeaconConfigPolicy.kt:wb + NoNameShadowing:NetworkConfigItemList.kt:wb + NoNameShadowing:RadioConfigViewModel.kt:RadioConfigViewModel${ it.copy( isLocal = false, pioEnv = null, channelList = emptyList(), radioConfig = LocalConfig.Builder().build(), moduleConfig = LocalModuleConfig.Builder().build(), ) } + NoNameShadowing:UserConfigItemList.kt:wb ParameterNaming:ChannelConfigScreen.kt:onPositiveClicked: (List<ChannelSettings>) -> Unit ParameterNaming:CleanNodeDatabaseScreen.kt:onCheckedChanged: (Boolean) -> Unit ParameterNaming:CleanNodeDatabaseScreen.kt:onDaysChanged: (Float) -> Unit @@ -104,6 +101,24 @@ ReturnCount:RadioConfigViewModel.kt:RadioConfigViewModel$private fun processPacketResponse TooGenericExceptionCaught:DebugViewModel.kt:DebugViewModel$e: Exception TooManyFunctions:RadioConfigViewModel.kt:RadioConfigViewModel : ViewModel + UnnecessaryLaunchedEffect:ChannelConfigScreen.kt:LaunchedEffect + UnnecessaryLaunchedEffect:Debug.kt:LaunchedEffect + UnnecessaryLaunchedEffect:LicensedModeSetting.kt:LaunchedEffect + UnnecessaryLaunchedEffect:Logcat.kt:LaunchedEffect + UnnecessaryLaunchedEffect:PacketAuthenticitySetting.kt:LaunchedEffect + UnnecessaryLaunchedEffect:SettingsNavigation.kt:LaunchedEffect + UnnecessaryLaunchedEffect:SettingsSearchBar.kt:LaunchedEffect + UnnecessaryLaunchedEffect:TakPermissionUtil.kt:LaunchedEffect + UnusedPrivateProperty:DebugViewModel.kt:DebugViewModel$private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers + UnusedPrivateProperty:DebugViewModel.kt:DebugViewModel$private val meshLogPrefs: MeshLogPrefs + UnusedPrivateProperty:RadioConfigViewModel.kt:RadioConfigViewModel$private val locationRepository: LocationRepository + UnusedPrivateProperty:SettingsViewModel.kt:SettingsViewModel$private val isOtaCapableUseCase: IsOtaCapableUseCase + UnusedPrivateProperty:SettingsViewModel.kt:SettingsViewModel$private val meshLogPrefs: MeshLogPrefs + UnusedPrivateProperty:SettingsViewModel.kt:SettingsViewModel$private val nodeRepository: NodeRepository + UseOrEmpty:AboutLibrariesLoader.kt:SettingsRoute::class.java.getResource("/aboutlibraries.json")?.readText() ?: "" + UseOrEmpty:DebugViewModel.kt:DebugViewModel$user?.short_name?.takeIf { it.isNotEmpty() } ?: "" + UseOrEmpty:DebugViewModel.kt:LogSearchManager$log.decodedPayload?.let { regex.findAll(it).map { SearchMatch(logIndex, it.range.first, it.range.last, "decodedPayload") } } ?: emptySequence() + UseOrEmpty:SettingsScreen.kt:ourNode?.user?.short_name ?: "" ViewModelForwarding:AdministrationScreen.kt:AdminRouteItems(viewModel = viewModel, enabled = enabled, state = state, destNode = destNode) ViewModelForwarding:PositionConfigScreen.kt:DeviceLocationButton( viewModel = viewModel, enabled = state.connected, onLocationReceived = { locationInput = it }, ) ViewModelForwarding:SecurityConfigScreen.kt:SecurityKeyBackupActions( viewModel = viewModel, enabled = state.connected, securityConfig = securityConfig, ) diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 82e59f84c0..d62a1e80d3 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -42,6 +42,7 @@ import kotlinx.datetime.format import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel import org.koin.core.qualifier.named import org.meshtastic.core.common.di.GOOGLE_SERVICES_AVAILABLE import org.meshtastic.core.common.util.UnitsOverride @@ -61,10 +62,8 @@ import org.meshtastic.core.resources.help_and_documentation import org.meshtastic.core.resources.import_configuration import org.meshtastic.core.resources.node_layout_section_title import org.meshtastic.core.resources.preferences_language -import org.meshtastic.core.resources.remotely_administrating import org.meshtastic.core.resources.wifi_devices import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.icon.Device import org.meshtastic.core.ui.icon.FilterList @@ -73,12 +72,14 @@ import org.meshtastic.core.ui.icon.List import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.SettingsRemote import org.meshtastic.core.ui.icon.Wifi +import org.meshtastic.core.ui.util.isBluetoothSupported import org.meshtastic.feature.settings.component.AppInfoSection import org.meshtastic.feature.settings.component.AppearanceSettingsContent import org.meshtastic.feature.settings.component.ExpressiveSection import org.meshtastic.feature.settings.component.PermissionsSettingsContent import org.meshtastic.feature.settings.component.PersistenceSettingsContent import org.meshtastic.feature.settings.component.PrivacySettingsContent +import org.meshtastic.feature.settings.component.RadioAdminAppBar import org.meshtastic.feature.settings.component.ThemePickerDialog import org.meshtastic.feature.settings.component.UnitsOption import org.meshtastic.feature.settings.component.UnitsPickerDialog @@ -87,6 +88,8 @@ import org.meshtastic.feature.settings.navigation.ModuleRoute import org.meshtastic.feature.settings.radio.RadioConfigItemList import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.component.EditDeviceProfileDialog +import org.meshtastic.feature.settings.search.SettingsSearchBar +import org.meshtastic.feature.settings.search.SettingsSearchViewModel import org.meshtastic.feature.settings.util.LanguageUtils import org.meshtastic.feature.settings.util.LanguageUtils.languageMap import org.meshtastic.feature.settings.util.deviceProfileExportFileName @@ -128,7 +131,9 @@ fun SettingsScreen( val exportConfigLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { uri -> viewModel.exportProfile(uri.toKmpUri(), deviceProfile!!) } + val profile = deviceProfile + val uri = it.data?.data + if (uri != null && profile != null) viewModel.exportProfile(uri.toKmpUri(), profile) } } @@ -203,21 +208,16 @@ fun SettingsScreen( topBar = { // Show back arrow when remotely administering (caller supplies onBack and we're not on the local node). val showBack = onBack != null && !state.isLocal - MainAppBar( + RadioAdminAppBar( title = stringResource(Res.string.bottom_nav_settings), - subtitle = - if (state.isLocal) { - ourNode?.user?.long_name - } else { - val remoteName = destNode?.user?.long_name ?: "" - stringResource(Res.string.remotely_administrating, remoteName) - }, + isLocal = state.isLocal, + destNode = destNode, + onNavigateUp = { onBack?.invoke() }, + localSubtitle = ourNode?.user?.long_name, ourNode = ourNode, + onClickChip = { node -> onClickNodeChip(node.num) }, showNodeChip = ourNode != null && isConnected && state.isLocal, canNavigateUp = showBack, - onNavigateUp = { onBack?.invoke() }, - actions = {}, - onClickChip = { node -> onClickNodeChip(node.num) }, ) }, ) { paddingValues -> @@ -225,6 +225,13 @@ fun SettingsScreen( modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { + SettingsSearchBar( + viewModel = koinViewModel(), + onNavigate = onNavigate, + // This phone's own settings are hidden below while administering another node; search hides them too. + includeAppLocal = state.isLocal, + ) + RadioConfigItemList( state = state, isManaged = localConfig.security?.is_managed ?: false, @@ -258,16 +265,17 @@ fun SettingsScreen( // App-local settings are only relevant when configuring the local node if (state.isLocal) { + val provideLocation = settingsViewModel.provideLocation.collectAsStateWithLifecycle().value // Ahead of the app settings block: onboarding runs once, so this is the only place a user who skipped // or declined a permission can find their way back to it. - PermissionsSettingsContent() + PermissionsSettingsContent(needsPreciseLocation = provideLocation) ExpressiveSection(title = stringResource(Res.string.app_settings)) { PrivacySettingsContent( analyticsAvailable = appFunctionsAvailable, analyticsEnabled = viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(true).value, onToggleAnalytics = { viewModel.toggleAnalyticsAllowed() }, - provideLocation = settingsViewModel.provideLocation.collectAsStateWithLifecycle().value, + provideLocation = provideLocation, onToggleLocation = { settingsViewModel.setProvideLocation(it) }, homoglyphEnabled = viewModel.homoglyphEncodingEnabledFlow.collectAsStateWithLifecycle(false).value, @@ -297,8 +305,11 @@ fun SettingsScreen( ) { onNavigate(SettingsRoute.NodeList) } - ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) { - onNavigate(WifiProvisionRoute.WifiProvision()) + // Wi-Fi provisioning reaches the device over BLE. + if (isBluetoothSupported()) { + ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) { + onNavigate(WifiProvisionRoute.WifiProvision()) + } } ListItem( text = stringResource(Res.string.filter_settings), diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt index 6bf098b0f5..61182bb1fe 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt @@ -22,37 +22,23 @@ import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.about import org.meshtastic.core.resources.app_notifications -import org.meshtastic.core.resources.app_version import org.meshtastic.core.resources.info import org.meshtastic.core.resources.intro_show -import org.meshtastic.core.resources.modules_already_unlocked -import org.meshtastic.core.resources.modules_unlocked import org.meshtastic.core.resources.system_settings import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.icon.AppSettingsAlt import org.meshtastic.core.ui.icon.ChevronRight import org.meshtastic.core.ui.icon.Info -import org.meshtastic.core.ui.icon.Memory import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Notifications import org.meshtastic.core.ui.icon.WavingHand import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.core.ui.util.showToast -import kotlin.time.Duration.Companion.seconds /** Section displaying application information and related actions. */ @Composable @@ -114,50 +100,6 @@ fun AppInfoSection( } } -private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules. -private const val UNLOCKED_CLICK_COUNT = 3 // Number of clicks before we toast that modules are already unlocked. -private const val UNLOCK_TIMEOUT_SECONDS = 1 // Timeout in seconds to reset the click counter. - -@Composable -private fun AppVersionButton( - hiddenFeaturesUnlocked: Boolean, - appVersionName: String, - onUnlockHiddenFeatures: () -> Unit, -) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - var clickCount by remember { mutableIntStateOf(0) } - - LaunchedEffect(clickCount) { - if (clickCount in 1.. { - clickCount = 0 - scope.launch { context.showToast(Res.string.modules_already_unlocked) } - } - - clickCount == UNLOCK_CLICK_COUNT -> { - clickCount = 0 - onUnlockHiddenFeatures() - scope.launch { context.showToast(Res.string.modules_unlocked) } - } - } - } -} - @Preview(showBackground = true) @Composable fun AppInfoSectionPreview() { diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt index 147465824a..c710cd0e4b 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt @@ -16,10 +16,10 @@ */ package org.meshtastic.feature.settings.component +import android.app.Activity.RESULT_OK import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview @@ -43,16 +43,15 @@ import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.rememberSaveFileLauncher import kotlin.time.Instant.Companion.fromEpochMilliseconds -private val EXPORT_TIMESTAMP_FORMAT = - LocalDateTime.Format { - year() - monthNumber() - day() - char('_') - hour() - minute() - second() - } +private val EXPORT_TIMESTAMP_FORMAT = LocalDateTime.Format { + year() + monthNumber() + day() + char('_') + hour() + minute() + second() +} /** Section for settings related to data persistence and exports. */ @Composable diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt index 4c3b76081e..d5d8c91539 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -16,63 +16,9 @@ */ package org.meshtastic.feature.settings.debugging -import android.content.Context -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.meshtastic.core.common.util.ioDispatcher -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.debug_export_failed -import org.meshtastic.core.resources.debug_logs_exported -import org.meshtastic.core.ui.util.showToast -import java.io.OutputStreamWriter -import java.nio.charset.StandardCharsets import java.util.concurrent.TimeUnit -@Composable -actual fun rememberLogExporter(contentProvider: suspend () -> String): (fileName: String) -> Unit { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val exportLogsLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri -> - if (createdUri != null) { - scope.launch { exportTextToUri(context, createdUri, contentProvider()) } - } - } - return { fileName -> exportLogsLauncher.launch(fileName) } -} - -private suspend fun exportTextToUri(context: Context, targetUri: Uri, content: String) = withContext(ioDispatcher) { - try { - if (content.isBlank()) { - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") } - Logger.w { "Log export aborted: no content" } - return@withContext - } - val stream = context.contentResolver.openOutputStream(targetUri) - if (stream == null) { - Logger.w { "Log export aborted: could not open output stream for $targetUri" } - withContext(Dispatchers.Main) { - context.showToast(Res.string.debug_export_failed, "Could not open file") - } - return@withContext - } - stream.use { os -> OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer -> writer.write(content) } } - Logger.i { "Logs exported successfully to $targetUri" } - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_logs_exported) } - } catch (e: java.io.IOException) { - Logger.e(e) { "Failed to export logs to URI: $targetUri" } - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, e.message ?: "") } - } -} - /** * Dumps this app's own logcat, filtered to our process id via `--pid` (API 24+, minSdk is 26). Without READ_LOGS the OS * already limits us to our own entries, but `--pid` guarantees it even if that permission is ever granted (e.g. via adb diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.android.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.android.kt index 09b3db9fa9..de3c0500ca 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.android.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.android.kt @@ -17,12 +17,10 @@ package org.meshtastic.feature.settings.radio.component import android.annotation.SuppressLint -import android.os.Build import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.core.location.LocationCompat import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Position @@ -49,14 +47,7 @@ actual fun DeviceLocationButton( Position( latitude = phoneLoc.latitude, longitude = phoneLoc.longitude, - altitude = - LocationCompat.hasMslAltitude(phoneLoc).let { - if (it && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - phoneLoc.mslAltitudeMeters.toInt() - } else { - phoneLoc.altitude.toInt() - } - }, + altitude = (phoneLoc.mslAltitudeMeters ?: phoneLoc.altitudeMeters ?: 0.0).toInt(), ) onLocationReceived(locationInput) } diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt deleted file mode 100644 index a28a576788..0000000000 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.settings.tak - -import android.content.Context -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -import co.touchlab.kermit.Logger -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.meshtastic.core.common.util.ioDispatcher - -@Composable -actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val exportLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { createdUri -> - if (createdUri != null) { - scope.launch { exportZipToUri(context, createdUri, dataPackageProvider()) } - } - } - return { fileName -> exportLauncher.launch(fileName) } -} - -private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(ioDispatcher) { - try { - context.contentResolver.openOutputStream(targetUri)?.use { os -> os.write(data) } - Logger.i { "TAK data package exported successfully to $targetUri" } - } catch (e: java.io.IOException) { - Logger.e(e) { "Failed to export TAK data package to URI: $targetUri" } - } -} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt index 12943e3469..4a458bdd35 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -44,10 +45,10 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration import org.meshtastic.core.resources.preserve_favorites -import org.meshtastic.core.resources.remotely_administrating +import org.meshtastic.core.resources.reboot_into_dfu_warning import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.feature.settings.component.ExpressiveSection +import org.meshtastic.feature.settings.component.RadioAdminAppBar import org.meshtastic.feature.settings.radio.AdminRoute import org.meshtastic.feature.settings.radio.RadioConfigState import org.meshtastic.feature.settings.radio.RadioConfigViewModel @@ -66,21 +67,11 @@ fun AdministrationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { Box(modifier = Modifier.fillMaxSize()) { Scaffold( topBar = { - MainAppBar( + RadioAdminAppBar( title = stringResource(Res.string.administration), - subtitle = - if (state.isLocal) { - destNode?.user?.long_name - } else { - val remoteName = destNode?.user?.long_name ?: "" - stringResource(Res.string.remotely_administrating, remoteName) - }, - ourNode = null, - showNodeChip = false, - canNavigateUp = true, + isLocal = state.isLocal, + destNode = destNode, onNavigateUp = onBack, - actions = {}, - onClickChip = {}, ) }, ) { paddingValues -> @@ -119,30 +110,53 @@ private fun AdminRouteItems( state: RadioConfigState, destNode: Node?, ) { - AdminRoute.entries.forEach { route -> - var showDialog by remember { mutableStateOf(false) } - if (showDialog) { - AdminActionDialog( - route = route, - destNode = destNode, - enabled = enabled, - state = state, - onDismiss = { showDialog = false }, - onConfirm = { viewModel.setResponseStateLoading(route) }, - onPreserveFavoritesChange = { viewModel.setPreserveFavorites(it) }, - ) + AdminRoute.entries + .filter { it != AdminRoute.REBOOT_DFU || state.canRebootToDfu } + .forEach { route -> + key(route) { + AdminRouteItem( + route = route, + destNode = destNode, + enabled = enabled, + state = state, + onConfirm = { viewModel.setResponseStateLoading(route) }, + onPreserveFavoritesChange = { viewModel.setPreserveFavorites(it) }, + ) + } } +} - ListItem( +@Composable +private fun AdminRouteItem( + route: AdminRoute, + destNode: Node?, + enabled: Boolean, + state: RadioConfigState, + onConfirm: () -> Unit, + onPreserveFavoritesChange: (Boolean) -> Unit, +) { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) { + AdminActionDialog( + route = route, + destNode = destNode, enabled = enabled, - text = stringResource(route.title), - leadingIcon = vectorResource(route.icon), - leadingIconTint = MaterialTheme.colorScheme.error, - textColor = MaterialTheme.colorScheme.error, - trailingIcon = null, - ) { - showDialog = true - } + state = state, + onDismiss = { showDialog = false }, + onConfirm = onConfirm, + onPreserveFavoritesChange = onPreserveFavoritesChange, + ) + } + + ListItem( + enabled = enabled, + text = stringResource(route.title), + leadingIcon = vectorResource(route.icon), + leadingIconTint = MaterialTheme.colorScheme.error, + textColor = MaterialTheme.colorScheme.error, + trailingIcon = null, + ) { + showDialog = true } } @@ -156,12 +170,13 @@ private fun AdminActionDialog( onConfirm: () -> Unit, onPreserveFavoritesChange: (Boolean) -> Unit, ) { - if (route == AdminRoute.SHUTDOWN || route == AdminRoute.REBOOT) { + if (route == AdminRoute.SHUTDOWN || route == AdminRoute.REBOOT || route == AdminRoute.REBOOT_DFU) { ShutdownConfirmationDialog( title = "${stringResource(route.title)}?", node = destNode, onDismiss = onDismiss, isShutdown = route == AdminRoute.SHUTDOWN, + warning = Res.string.reboot_into_dfu_warning.takeIf { route == AdminRoute.REBOOT_DFU }, onConfirm = onConfirm, ) } else { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt index 0457daca41..d7d9ee29e9 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt @@ -32,10 +32,9 @@ import org.jetbrains.compose.resources.vectorResource import org.meshtastic.core.navigation.Route import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.device_configuration -import org.meshtastic.core.resources.remotely_administrating import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.feature.settings.component.ExpressiveSection +import org.meshtastic.feature.settings.component.RadioAdminAppBar import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.radio.RadioConfigViewModel @@ -46,21 +45,11 @@ fun DeviceConfigurationScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni Scaffold( topBar = { - MainAppBar( + RadioAdminAppBar( title = stringResource(Res.string.device_configuration), - subtitle = - if (state.isLocal) { - destNode?.user?.long_name - } else { - val remoteName = destNode?.user?.long_name ?: "" - stringResource(Res.string.remotely_administrating, remoteName) - }, - ourNode = null, - showNodeChip = false, - canNavigateUp = true, + isLocal = state.isLocal, + destNode = destNode, onNavigateUp = onBack, - actions = {}, - onClickChip = {}, ) }, ) { paddingValues -> diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt index b279604a78..b4c7ed0467 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt @@ -33,10 +33,9 @@ import org.jetbrains.compose.resources.vectorResource import org.meshtastic.core.navigation.Route import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.module_settings -import org.meshtastic.core.resources.remotely_administrating import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.feature.settings.component.ExpressiveSection +import org.meshtastic.feature.settings.component.RadioAdminAppBar import org.meshtastic.feature.settings.navigation.ModuleRoute import org.meshtastic.feature.settings.radio.RadioConfigViewModel @@ -62,21 +61,11 @@ fun ModuleConfigurationScreen( Scaffold( topBar = { - MainAppBar( + RadioAdminAppBar( title = stringResource(Res.string.module_settings), - subtitle = - if (state.isLocal) { - destNode?.user?.long_name - } else { - val remoteName = destNode?.user?.long_name ?: "" - stringResource(Res.string.remotely_administrating, remoteName) - }, - ourNode = null, - showNodeChip = false, - canNavigateUp = true, + isLocal = state.isLocal, + destNode = destNode, onNavigateUp = onBack, - actions = {}, - onClickChip = {}, ) }, ) { paddingValues -> diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index 2ae3499b41..a244949949 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -141,12 +141,12 @@ class SettingsViewModel( val meshLogLoggingEnabled: StateFlow = _meshLogLoggingEnabled.asStateFlow() fun setMeshLogRetentionDays(days: Int) { - safeLaunch(tag = "setMeshLogRetentionDays") { setMeshLogSettingsUseCase.setRetentionDays(days) } + setMeshLogSettingsUseCase.setRetentionDays(days) _meshLogRetentionDays.value = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS) } fun setMeshLogLoggingEnabled(enabled: Boolean) { - safeLaunch(tag = "setMeshLogLoggingEnabled") { setMeshLogSettingsUseCase.setLoggingEnabled(enabled) } + setMeshLogSettingsUseCase.setLoggingEnabled(enabled) _meshLogLoggingEnabled.value = enabled } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt index 7de8cef928..d4480ca399 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.repository.AppFunctionsSetting import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.app_functions_get_channel_info import org.meshtastic.core.resources.app_functions_get_device_status @@ -87,33 +88,33 @@ fun AppFunctionsSettingsScreen( Column(modifier = Modifier.padding(padding).verticalScroll(rememberScrollState())) { MasterToggleSection( masterEnabled = masterEnabled, - onToggle = { viewModel.setMasterEnabled(!masterEnabled) }, + onToggle = { viewModel.toggle(AppFunctionsSetting.MASTER) }, ) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) WriteFunctionsSection( masterEnabled = masterEnabled, sendMessage = sendMessage, - onToggleSendMessage = { viewModel.setSendMessageEnabled(!sendMessage) }, + onToggleSendMessage = { viewModel.toggle(AppFunctionsSetting.SEND_MESSAGE) }, ) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) ReadFunctionsSection( masterEnabled = masterEnabled, getMeshStatus = getMeshStatus, - onToggleMeshStatus = { viewModel.setGetMeshStatusEnabled(!getMeshStatus) }, + onToggleMeshStatus = { viewModel.toggle(AppFunctionsSetting.GET_MESH_STATUS) }, getNodeList = getNodeList, - onToggleNodeList = { viewModel.setGetNodeListEnabled(!getNodeList) }, + onToggleNodeList = { viewModel.toggle(AppFunctionsSetting.GET_NODE_LIST) }, getChannelInfo = getChannelInfo, - onToggleChannelInfo = { viewModel.setGetChannelInfoEnabled(!getChannelInfo) }, + onToggleChannelInfo = { viewModel.toggle(AppFunctionsSetting.GET_CHANNEL_INFO) }, getDeviceStatus = getDeviceStatus, - onToggleDeviceStatus = { viewModel.setGetDeviceStatusEnabled(!getDeviceStatus) }, + onToggleDeviceStatus = { viewModel.toggle(AppFunctionsSetting.GET_DEVICE_STATUS) }, getNodeDetails = getNodeDetails, - onToggleNodeDetails = { viewModel.setGetNodeDetailsEnabled(!getNodeDetails) }, + onToggleNodeDetails = { viewModel.toggle(AppFunctionsSetting.GET_NODE_DETAILS) }, getMeshMetrics = getMeshMetrics, - onToggleMeshMetrics = { viewModel.setGetMeshMetricsEnabled(!getMeshMetrics) }, + onToggleMeshMetrics = { viewModel.toggle(AppFunctionsSetting.GET_MESH_METRICS) }, getRecentMessages = getRecentMessages, - onToggleRecentMessages = { viewModel.setGetRecentMessagesEnabled(!getRecentMessages) }, + onToggleRecentMessages = { viewModel.toggle(AppFunctionsSetting.GET_RECENT_MESSAGES) }, getUnreadSummary = getUnreadSummary, - onToggleUnreadSummary = { viewModel.setGetUnreadSummaryEnabled(!getUnreadSummary) }, + onToggleUnreadSummary = { viewModel.toggle(AppFunctionsSetting.GET_UNREAD_SUMMARY) }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModel.kt index 4069f63d9e..f479b6ce65 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModel.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.repository.AppFunctionsPrefs +import org.meshtastic.core.repository.AppFunctionsSetting @KoinViewModel class AppFunctionsSettingsViewModel(private val prefs: AppFunctionsPrefs) : ViewModel() { @@ -35,23 +36,5 @@ class AppFunctionsSettingsViewModel(private val prefs: AppFunctionsPrefs) : View val getRecentMessagesEnabled: StateFlow = prefs.getRecentMessagesEnabled val getUnreadSummaryEnabled: StateFlow = prefs.getUnreadSummaryEnabled - fun setMasterEnabled(enabled: Boolean) = prefs.setMasterEnabled(enabled) - - fun setSendMessageEnabled(enabled: Boolean) = prefs.setSendMessageEnabled(enabled) - - fun setGetMeshStatusEnabled(enabled: Boolean) = prefs.setGetMeshStatusEnabled(enabled) - - fun setGetNodeListEnabled(enabled: Boolean) = prefs.setGetNodeListEnabled(enabled) - - fun setGetChannelInfoEnabled(enabled: Boolean) = prefs.setGetChannelInfoEnabled(enabled) - - fun setGetDeviceStatusEnabled(enabled: Boolean) = prefs.setGetDeviceStatusEnabled(enabled) - - fun setGetNodeDetailsEnabled(enabled: Boolean) = prefs.setGetNodeDetailsEnabled(enabled) - - fun setGetMeshMetricsEnabled(enabled: Boolean) = prefs.setGetMeshMetricsEnabled(enabled) - - fun setGetRecentMessagesEnabled(enabled: Boolean) = prefs.setGetRecentMessagesEnabled(enabled) - - fun setGetUnreadSummaryEnabled(enabled: Boolean) = prefs.setGetUnreadSummaryEnabled(enabled) + fun toggle(setting: AppFunctionsSetting) = prefs.toggle(setting) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/AppVersionButton.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/AppVersionButton.kt new file mode 100644 index 0000000000..dbf11491e9 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/AppVersionButton.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.app_version +import org.meshtastic.core.resources.modules_already_unlocked +import org.meshtastic.core.resources.modules_unlocked +import org.meshtastic.core.ui.component.ListItem +import org.meshtastic.core.ui.icon.Memory +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.util.rememberShowToastResource +import kotlin.time.Duration.Companion.seconds + +private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules. +private const val UNLOCKED_CLICK_COUNT = 3 // Number of clicks before we toast that modules are already unlocked. +private const val UNLOCK_TIMEOUT_SECONDS = 1 // Timeout in seconds to reset the click counter. + +/** The app version row, which unlocks hidden features after [UNLOCK_CLICK_COUNT] quick taps. */ +@Composable +internal fun AppVersionButton( + hiddenFeaturesUnlocked: Boolean, + appVersionName: String, + onUnlockHiddenFeatures: () -> Unit, +) { + val scope = rememberCoroutineScope() + val showToast = rememberShowToastResource() + var clickCount by remember { mutableIntStateOf(0) } + + LaunchedEffect(clickCount) { + if (clickCount in 1.. { + clickCount = 0 + scope.launch { showToast(Res.string.modules_already_unlocked) } + } + + clickCount == UNLOCK_CLICK_COUNT -> { + clickCount = 0 + onUnlockHiddenFeatures() + scope.launch { showToast(Res.string.modules_unlocked) } + } + } + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PermissionsSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PermissionsSection.kt index 8ad16a2c17..49169f4527 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PermissionsSection.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PermissionsSection.kt @@ -38,6 +38,7 @@ import org.meshtastic.core.resources.local_network_permission import org.meshtastic.core.resources.local_network_permission_rationale import org.meshtastic.core.resources.location_permission import org.meshtastic.core.resources.location_permission_rationale +import org.meshtastic.core.resources.location_precise_rationale import org.meshtastic.core.resources.nearby_devices_permission import org.meshtastic.core.resources.notification_permission_rationale import org.meshtastic.core.resources.permission_camera_summary @@ -47,6 +48,7 @@ import org.meshtastic.core.resources.permission_location_summary_pre31 import org.meshtastic.core.resources.permission_nearby_devices_summary import org.meshtastic.core.resources.permission_notifications_summary import org.meshtastic.core.resources.permission_state_allowed +import org.meshtastic.core.resources.permission_state_approximate_only import org.meshtastic.core.resources.permission_state_blocked import org.meshtastic.core.resources.permission_state_denied import org.meshtastic.core.resources.permission_state_not_applicable @@ -71,12 +73,14 @@ import org.meshtastic.core.ui.util.PermissionGateAction import org.meshtastic.core.ui.util.PermissionStatus import org.meshtastic.core.ui.util.PermissionUiState import org.meshtastic.core.ui.util.bleScanRequiresLocationServices +import org.meshtastic.core.ui.util.isBluetoothSupported import org.meshtastic.core.ui.util.permissionGateAction import org.meshtastic.core.ui.util.rememberBluetoothPermissionState import org.meshtastic.core.ui.util.rememberCameraPermissionState import org.meshtastic.core.ui.util.rememberLocalNetworkPermissionState import org.meshtastic.core.ui.util.rememberLocationPermissionState import org.meshtastic.core.ui.util.rememberNotificationPermissionState +import org.meshtastic.core.ui.util.rememberPreciseLocationPermissionState /** * One permission the app can ask for, as the permissions list needs to render it. @@ -84,6 +88,7 @@ import org.meshtastic.core.ui.util.rememberNotificationPermissionState * @param titleRes The permission's name as the system presents it, so the row and the system screen agree. * @param summaryRes What the app does with it, in the user's terms. * @param rationaleRes The educational text shown before a re-request. + * @param statusRes Replaces the state label derived from [state] when the grant needs a fuller explanation. */ private data class PermissionRow( val titleRes: StringResource, @@ -91,6 +96,7 @@ private data class PermissionRow( val rationaleRes: StringResource, val icon: ImageVector, val state: PermissionUiState, + val statusRes: StringResource? = null, ) /** @@ -106,9 +112,10 @@ private data class PermissionRow( * because reviewing and revoking is as legitimate as granting. */ @Composable -internal fun ColumnScope.PermissionsSettingsContent() { +internal fun ColumnScope.PermissionsSettingsContent(needsPreciseLocation: Boolean = false) { val bluetooth = rememberBluetoothPermissionState() val location = rememberLocationPermissionState() + val preciseLocation = rememberPreciseLocationPermissionState() val notifications = rememberNotificationPermissionState() val camera = rememberCameraPermissionState() val localNetwork = rememberLocalNetworkPermissionState() @@ -117,14 +124,19 @@ internal fun ColumnScope.PermissionsSettingsContent() { // that does not exist on that device. val bluetoothIsLocation = bleScanRequiresLocationServices + // An approximate grant serves the map but not mesh sharing, so while sharing is on the row stands for precise. + val locationApproximateOnly = needsPreciseLocation && location.isGranted && !preciseLocation.isGranted + val rows = permissionRows( bluetooth = bluetooth, - location = location, + location = if (locationApproximateOnly) preciseLocation else location, + locationApproximateOnly = locationApproximateOnly, notifications = notifications, camera = camera, localNetwork = localNetwork, bluetoothIsLocation = bluetoothIsLocation, + bluetoothSupported = isBluetoothSupported(), ) // Rows that need nothing from the user are collapsed by default. Five rows reading "Allowed" is a wall every @@ -195,11 +207,14 @@ internal fun ColumnScope.PermissionsSettingsContent() { private fun permissionRows( bluetooth: PermissionUiState, location: PermissionUiState, + locationApproximateOnly: Boolean, notifications: PermissionUiState, camera: PermissionUiState, localNetwork: PermissionUiState, bluetoothIsLocation: Boolean, + bluetoothSupported: Boolean, ): List = buildList { + val locationStatusRes = Res.string.permission_state_approximate_only.takeIf { locationApproximateOnly } // Pre-Android-12 the Bluetooth gate *is* ACCESS_FINE_LOCATION. Two rows there would offer two controls for // one system grant and let them contradict each other on screen, so a single Location row stands for both. if (bluetoothIsLocation) { @@ -210,25 +225,34 @@ private fun permissionRows( rationaleRes = Res.string.bluetooth_permission_rationale_pre31, icon = MeshtasticIcons.LocationOn, state = location, + statusRes = locationStatusRes, ), ) } else { - add( - PermissionRow( - titleRes = Res.string.nearby_devices_permission, - summaryRes = Res.string.permission_nearby_devices_summary, - rationaleRes = Res.string.bluetooth_permission_rationale, - icon = MeshtasticIcons.Bluetooth, - state = bluetooth, - ), - ) + if (bluetoothSupported) { + add( + PermissionRow( + titleRes = Res.string.nearby_devices_permission, + summaryRes = Res.string.permission_nearby_devices_summary, + rationaleRes = Res.string.bluetooth_permission_rationale, + icon = MeshtasticIcons.Bluetooth, + state = bluetooth, + ), + ) + } add( PermissionRow( titleRes = Res.string.location_permission, summaryRes = Res.string.permission_location_summary, - rationaleRes = Res.string.location_permission_rationale, + rationaleRes = + if (locationApproximateOnly) { + Res.string.location_precise_rationale + } else { + Res.string.location_permission_rationale + }, icon = MeshtasticIcons.LocationOn, state = location, + statusRes = locationStatusRes, ), ) } @@ -275,7 +299,7 @@ private fun PermissionListItem(row: PermissionRow, onShowRationale: () -> Unit) val state = row.state val gated = state.isRuntimeGated - val statusRes = permissionStateLabel(state.status, gated) + val statusRes = row.statusRes ?: permissionStateLabel(state.status, gated) // Only a blocked permission is coloured. Denied is a choice the user made and is free to keep — colouring it red // would read as a scolding, which the permissions guidance explicitly warns against. diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt index 3c3c62d03c..a7f653123f 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt @@ -29,8 +29,8 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.analytics_okay import org.meshtastic.core.resources.location_disabled import org.meshtastic.core.resources.location_permission -import org.meshtastic.core.resources.location_permission_blocked_toast -import org.meshtastic.core.resources.location_permission_rationale +import org.meshtastic.core.resources.location_precise_blocked_toast +import org.meshtastic.core.resources.location_precise_rationale import org.meshtastic.core.resources.provide_location_to_mesh import org.meshtastic.core.ui.component.PermissionRationaleDialog import org.meshtastic.core.ui.component.SwitchListItem @@ -40,7 +40,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.util.PermissionGateAction import org.meshtastic.core.ui.util.isGpsDisabled import org.meshtastic.core.ui.util.permissionGateAction -import org.meshtastic.core.ui.util.rememberLocationPermissionState +import org.meshtastic.core.ui.util.rememberPreciseLocationPermissionState import org.meshtastic.core.ui.util.rememberShowToastResource /** Section managing privacy settings like analytics and location sharing. */ @@ -57,7 +57,8 @@ internal fun ColumnScope.PrivacySettingsContent( stopProvideLocation: () -> Unit, ) { val showToast = rememberShowToastResource() - val locationPermission = rememberLocationPermissionState() + // Mesh sharing needs precise location: the service drops an approximate-only grant, so this toggle asks for fine. + val locationPermission = rememberPreciseLocationPermissionState() val isGpsOff = isGpsDisabled() // Captured through rememberUpdatedState: the effect below restarts on every status change, and reading the @@ -72,7 +73,7 @@ internal fun ColumnScope.PrivacySettingsContent( if (showLocationRationale) { PermissionRationaleDialog( titleRes = Res.string.location_permission, - rationaleRes = Res.string.location_permission_rationale, + rationaleRes = Res.string.location_precise_rationale, icon = MeshtasticIcons.LocationOn, onConfirm = { showLocationRationale = false @@ -106,7 +107,7 @@ internal fun ColumnScope.PrivacySettingsContent( // put it back until it can be. PermissionGateAction.OPEN_SETTINGS -> { currentToggleLocation(false) - currentShowToast(Res.string.location_permission_blocked_toast) + currentShowToast(Res.string.location_precise_blocked_toast) locationPermission.openAppSettings() } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/RadioAdminAppBar.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/RadioAdminAppBar.kt new file mode 100644 index 0000000000..5eab39dfa6 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/RadioAdminAppBar.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Node +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.remotely_administrating +import org.meshtastic.core.ui.component.MainAppBar + +/** + * App bar for a screen that configures either the connected node or a remote one: while [isLocal] the subtitle is + * [localSubtitle], otherwise it names [destNode] as the node being administered. + */ +@Composable +internal fun RadioAdminAppBar( + title: String, + isLocal: Boolean, + destNode: Node?, + onNavigateUp: () -> Unit, + modifier: Modifier = Modifier, + localSubtitle: String? = destNode?.user?.long_name, + ourNode: Node? = null, + onClickChip: (Node) -> Unit = {}, + showNodeChip: Boolean = false, + canNavigateUp: Boolean = true, +) { + MainAppBar( + modifier = modifier, + title = title, + subtitle = + if (isLocal) { + localSubtitle + } else { + stringResource(Res.string.remotely_administrating, destNode?.user?.long_name.orEmpty()) + }, + ourNode = ourNode, + showNodeChip = showNodeChip, + canNavigateUp = canNavigateUp, + onNavigateUp = onNavigateUp, + onClickChip = onClickChip, + actions = {}, + ) +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt index 14b7b1b558..66390eeb52 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt @@ -108,6 +108,7 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { val searchState by viewModel.searchState.collectAsStateWithLifecycle() val filterTexts by viewModel.filterTexts.collectAsStateWithLifecycle() val selectedLogId by viewModel.selectedLogId.collectAsStateWithLifecycle() + val searchTerms = remember(searchState.searchText) { compileSearchTerms(searchState.searchText) } var filterMode by remember { mutableStateOf(FilterMode.OR) } @@ -225,7 +226,7 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { DebugItem( modifier = Modifier.animateItem(), log = log, - searchText = searchState.searchText, + searchTerms = searchTerms, isSelected = selectedLogId == log.uuid, onLogClick = { viewModel.setSelectedLogId(if (selectedLogId == log.uuid) null else log.uuid) }, ) @@ -277,7 +278,7 @@ private fun DebugLogSettings(viewModel: DebugViewModel) { internal fun DebugItem( log: UiMeshLog, modifier: Modifier = Modifier, - searchText: String = "", + searchTerms: List = emptyList(), isSelected: Boolean = false, onLogClick: () -> Unit = {}, ) { @@ -305,8 +306,8 @@ internal fun DebugItem( Column( modifier = Modifier.padding(if (isSelected) 12.dp else 8.dp).fillMaxWidth().clickable { onLogClick() }, ) { - DebugItemHeader(log = log, searchText = searchText, isSelected = isSelected, theme = colorScheme) - val messageAnnotatedString = rememberAnnotatedLogMessage(log, searchText) + DebugItemHeader(log = log, searchTerms = searchTerms, isSelected = isSelected, theme = colorScheme) + val messageAnnotatedString = rememberAnnotatedLogMessage(log, searchTerms) Text( text = messageAnnotatedString, style = @@ -322,7 +323,7 @@ internal fun DebugItem( decodedPayload = log.decodedPayload, isSelected = isSelected, colorScheme = colorScheme, - searchText = searchText, + searchTerms = searchTerms, modifier = Modifier, ) } @@ -332,13 +333,13 @@ internal fun DebugItem( } @Composable -private fun DebugItemHeader(log: UiMeshLog, searchText: String, isSelected: Boolean, theme: ColorScheme) { +private fun DebugItemHeader(log: UiMeshLog, searchTerms: List, isSelected: Boolean, theme: ColorScheme) { Row( modifier = Modifier.fillMaxWidth().padding(bottom = if (isSelected) 12.dp else 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val typeAnnotatedString = rememberAnnotatedString(text = log.messageType, searchText = searchText) + val typeAnnotatedString = rememberAnnotatedString(text = log.messageType, searchTerms = searchTerms) Text( text = typeAnnotatedString, modifier = Modifier.weight(1f), @@ -353,7 +354,7 @@ private fun DebugItemHeader(log: UiMeshLog, searchText: String, isSelected: Bool // it must not carry key material either. Marked sensitive so the OS does not surface it in a paste preview. val fullLogText = remember(log.logMessage, log.decodedPayload) { formatLogEntryForCopy(log) } CopyIconButton(valueToCopy = fullLogText, modifier = Modifier.padding(start = 8.dp), sensitive = true) - val dateAnnotatedString = rememberAnnotatedString(text = log.formattedReceivedDate, searchText = searchText) + val dateAnnotatedString = rememberAnnotatedString(text = log.formattedReceivedDate, searchTerms = searchTerms) Text( text = dateAnnotatedString, style = @@ -367,31 +368,40 @@ private fun DebugItemHeader(log: UiMeshLog, searchText: String, isSelected: Bool } @Composable -private fun rememberAnnotatedString(text: String, searchText: String): AnnotatedString { +private fun rememberAnnotatedString(text: String, searchTerms: List): AnnotatedString { val theme = MaterialTheme.colorScheme val highlightStyle = SpanStyle(background = theme.primary.copy(alpha = 0.3f), color = theme.onSurface) - return remember(text, searchText) { + return remember(text, searchTerms) { buildAnnotatedString { append(text) - if (searchText.isNotEmpty()) { - searchText.split(" ").forEach { term -> - Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(text).forEach { match -> - addStyle(style = highlightStyle, start = match.range.first, end = match.range.last + 1) - } - } - } + highlightMatches(text, searchTerms, highlightStyle) + } + } +} + +/** + * Compiles each search term once per query, so every log row shares the same patterns instead of compiling its own. + * Blank terms are dropped: an empty pattern matches at every index. + */ +internal fun compileSearchTerms(searchText: String): List = + searchText.split(" ").filter { it.isNotEmpty() }.map { Regex(Regex.escape(it), RegexOption.IGNORE_CASE) } + +private fun AnnotatedString.Builder.highlightMatches(text: String, searchTerms: List, style: SpanStyle) { + searchTerms.forEach { term -> + term.findAll(text).forEach { match -> + addStyle(style = style, start = match.range.first, end = match.range.last + 1) } } } @Composable -private fun rememberAnnotatedLogMessage(log: UiMeshLog, searchText: String): AnnotatedString { +private fun rememberAnnotatedLogMessage(log: UiMeshLog, searchTerms: List): AnnotatedString { val theme = MaterialTheme.colorScheme val style = SpanStyle(color = AnnotationColor, fontStyle = FontStyle.Italic) val highlightStyle = SpanStyle(background = theme.primary.copy(alpha = 0.3f), color = theme.onSurface) - return remember(log.uuid, searchText) { + return remember(log.uuid, searchTerms) { buildAnnotatedString { append(log.logMessage) @@ -400,14 +410,7 @@ private fun rememberAnnotatedLogMessage(log: UiMeshLog, searchText: String): Ann addStyle(style = style, start = it.range.first, end = it.range.last + 1) } - // Add search highlight annotations - if (searchText.isNotEmpty()) { - searchText.split(" ").forEach { term -> - Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(log.logMessage).forEach { match -> - addStyle(style = highlightStyle, start = match.range.first, end = match.range.last + 1) - } - } - } + highlightMatches(log.logMessage, searchTerms, highlightStyle) } } } @@ -424,7 +427,7 @@ private fun DecodedPayloadBlock( decodedPayload: String, isSelected: Boolean, colorScheme: ColorScheme, - searchText: String = "", + searchTerms: List = emptyList(), modifier: Modifier = Modifier, ) { val commonTextStyle = @@ -446,7 +449,7 @@ private fun DecodedPayloadBlock( modifier = Modifier.padding(top = 8.dp, bottom = 4.dp), ) Text(text = "{", style = commonTextStyle, modifier = Modifier.padding(start = 8.dp, bottom = 2.dp)) - val annotatedPayload = rememberAnnotatedDecodedPayload(decodedPayload, searchText, colorScheme) + val annotatedPayload = rememberAnnotatedDecodedPayload(decodedPayload, searchTerms, colorScheme) Text( text = annotatedPayload, softWrap = true, @@ -465,20 +468,14 @@ private fun DecodedPayloadBlock( @Composable private fun rememberAnnotatedDecodedPayload( decodedPayload: String, - searchText: String, + searchTerms: List, colorScheme: ColorScheme, ): AnnotatedString { val highlightStyle = SpanStyle(background = colorScheme.primary.copy(alpha = 0.3f), color = colorScheme.onSurface) - return remember(decodedPayload, searchText) { + return remember(decodedPayload, searchTerms) { buildAnnotatedString { append(decodedPayload) - if (searchText.isNotEmpty()) { - searchText.split(" ").forEach { term -> - Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(decodedPayload).forEach { match -> - addStyle(style = highlightStyle, start = match.range.first, end = match.range.last + 1) - } - } - } + highlightMatches(decodedPayload, searchTerms, highlightStyle) } } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index b81520c3ee..1ddb175b0b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -36,9 +36,10 @@ import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.common.util.nowInstant -import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase import org.meshtastic.core.model.MeshLog import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.getTracerouteResponse import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toReadableString @@ -219,6 +220,7 @@ class DebugViewModel( private val meshLogRepository: MeshLogRepository, private val nodeRepository: NodeRepository, private val meshLogPrefs: MeshLogPrefs, + private val setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase, private val alertManager: AlertManager, private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers, ) : ViewModel() { @@ -271,22 +273,13 @@ class DebugViewModel( } fun setRetentionDays(days: Int) { - val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS) - meshLogPrefs.setRetentionDays(clamped) - _retentionDays.value = clamped - safeLaunch(tag = "setRetentionDays") { meshLogRepository.deleteLogsOlderThan(clamped) } + setMeshLogSettingsUseCase.setRetentionDays(days) + _retentionDays.value = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS) } fun setLoggingEnabled(enabled: Boolean) { - meshLogPrefs.setLoggingEnabled(enabled) + setMeshLogSettingsUseCase.setLoggingEnabled(enabled) _loggingEnabled.value = enabled - if (!enabled) { - safeLaunch(tag = "disableLogging") { meshLogRepository.deleteAll() } - } else { - safeLaunch(tag = "enableLogging") { - meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) - } - } } suspend fun loadLogsForExport(): ImmutableList = withContext(ioDispatcher) { @@ -370,7 +363,7 @@ class DebugViewModel( val placeholder = "___RELAY_NODE___" if (relayNode != 0) { - Packet.getRelayNode(relayNode, nodeList, myNodeNum)?.let { node -> + Node.getRelayNode(relayNode, nodeList, myNodeNum)?.let { node -> val relayId = node.user.id val relayName = node.user.long_name // Wire's toString prints `relay_node=245`; rows stored before the Wire @@ -419,13 +412,11 @@ class DebugViewModel( if (!regex.containsMatchIn(this)) return false regex.findAll(this).toList().asReversed().forEach { val idx = it.range.last + 1 - insert(idx, " (${nodeId.toHex(8)})") + insert(idx, " (${NodeAddress.numToDefaultId(nodeId)})") } return true } - private fun Int.toHex(length: Int): String = "!${this.toUInt().toString(16).padStart(length, '0')}" - fun requestDeleteAllLogs() { alertManager.showAlert( titleRes = Res.string.debug_clear, @@ -448,7 +439,7 @@ class DebugViewModel( val presetFilters: List get() = buildList { // Our address if available - nodeRepository.myNodeInfo.value?.myNodeNum?.let { add(it.toHex(8)) } + nodeRepository.myNodeInfo.value?.myNodeNum?.let { add(NodeAddress.numToDefaultId(it)) } // broadcast add("!ffffffff") // decoded @@ -541,7 +532,7 @@ class DebugViewModel( private fun formatNodeWithShortName(nodeNum: Int): String { val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user val shortName = user?.short_name?.takeIf { it.isNotEmpty() } ?: "" - val nodeId = nodeNum.toHex(8) + val nodeId = NodeAddress.numToDefaultId(nodeNum) return if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExportLauncher.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExportLauncher.kt new file mode 100644 index 0000000000..114fca4f65 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExportLauncher.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.debugging + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.debug_export_failed +import org.meshtastic.core.resources.debug_logs_exported +import org.meshtastic.core.resources.getStringSuspend +import org.meshtastic.core.ui.util.rememberFileExporter +import org.meshtastic.core.ui.util.rememberShowToast + +/** + * Remembers a launcher that writes [contentProvider]'s text to a user-chosen file and toasts the outcome. Blank text is + * reported as a failure rather than written as an empty file. + */ +@Composable +fun rememberLogExporter(contentProvider: suspend () -> String): (fileName: String) -> Unit { + val showToast = rememberShowToast() + var requestedFileName by rememberSaveable { mutableStateOf("") } + val export = + rememberFileExporter( + content = { contentProvider().takeIf { it.isNotBlank() }?.encodeToByteArray() }, + onResult = { exported -> + showToast( + if (exported) { + getStringSuspend(Res.string.debug_logs_exported) + } else { + getStringSuspend(Res.string.debug_export_failed, requestedFileName) + }, + ) + }, + ) + return remember(export) { + { fileName -> + requestedFileName = fileName + export(fileName, "text/plain") + } + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt index 82b8286934..cd0d7c3041 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -16,11 +16,6 @@ */ package org.meshtastic.feature.settings.debugging -import androidx.compose.runtime.Composable - -/** Remembers a launcher that writes [contentProvider]'s text to a user-chosen file. */ -@Composable expect fun rememberLogExporter(contentProvider: suspend () -> String): (fileName: String) -> Unit - /** * The app's own logs: Android logcat (filtered to this process) on Android, an in-memory Kermit buffer on desktop. * Empty on platforms with no log capture (currently iOS). diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt index 621316fb10..844aa94a02 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt @@ -64,7 +64,8 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons @Composable fun FilterSettingsScreen(viewModel: FilterSettingsViewModel, onBack: () -> Unit) { val filterEnabled by viewModel.filterEnabled.collectAsStateWithLifecycle() - val filterWords by viewModel.filterWords.collectAsStateWithLifecycle() + val storedWords by viewModel.filterWords.collectAsStateWithLifecycle() + val filterWords = remember(storedWords) { storedWords.sorted() } var newWord by remember { mutableStateOf("") } Scaffold( diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt index dd88c4bb34..a7aeb613a2 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt @@ -17,9 +17,7 @@ package org.meshtastic.feature.settings.filter import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter @@ -28,15 +26,13 @@ import org.meshtastic.core.repository.MessageFilter class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val messageFilter: MessageFilter) : ViewModel() { - private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled.value) - val filterEnabled: StateFlow = _filterEnabled.asStateFlow() + // Read through, never snapshot: the prefs flows start at their defaults until DataStore loads. + val filterEnabled: StateFlow = filterPrefs.filterEnabled - private val _filterWords = MutableStateFlow(filterPrefs.filterWords.value.toList().sorted()) - val filterWords: StateFlow> = _filterWords.asStateFlow() + val filterWords: StateFlow> = filterPrefs.filterWords fun setFilterEnabled(enabled: Boolean) { filterPrefs.setFilterEnabled(enabled) - _filterEnabled.value = enabled } fun addFilterWord(word: String) { @@ -45,7 +41,6 @@ class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val val current = filterPrefs.filterWords.value.toMutableSet() if (current.add(trimmed)) { filterPrefs.setFilterWords(current) - _filterWords.value = current.toList().sorted() messageFilter.rebuildPatterns() } } @@ -54,7 +49,6 @@ class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val val current = filterPrefs.filterWords.value.toMutableSet() if (current.remove(word)) { filterPrefs.setFilterWords(current) - _filterWords.value = current.toList().sorted() messageFilter.rebuildPatterns() } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt index 79bf25320a..1dcc7db393 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt @@ -16,19 +16,11 @@ */ package org.meshtastic.feature.settings.lockdown -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -38,9 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.service.LockdownState @@ -48,23 +37,12 @@ import org.meshtastic.core.repository.LockdownPassphraseStore import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.disconnect import org.meshtastic.core.resources.lockdown_backoff -import org.meshtastic.core.resources.lockdown_boots_remaining -import org.meshtastic.core.resources.lockdown_confirm_passphrase import org.meshtastic.core.resources.lockdown_enter_passphrase -import org.meshtastic.core.resources.lockdown_hide_passphrase -import org.meshtastic.core.resources.lockdown_hours_until_expiry import org.meshtastic.core.resources.lockdown_incorrect_passphrase import org.meshtastic.core.resources.lockdown_lock_reason import org.meshtastic.core.resources.lockdown_passphrase -import org.meshtastic.core.resources.lockdown_passphrases_do_not_match -import org.meshtastic.core.resources.lockdown_session_minutes -import org.meshtastic.core.resources.lockdown_session_minutes_help import org.meshtastic.core.resources.lockdown_set_passphrase -import org.meshtastic.core.resources.lockdown_show_passphrase import org.meshtastic.core.resources.lockdown_submit -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Visibility -import org.meshtastic.core.ui.icon.VisibilityOff /** * Non-dismissable lockdown authentication dialog. @@ -103,9 +81,8 @@ fun LockdownDialog( val title = stringResource(if (isProvisioning) Res.string.lockdown_set_passphrase else Res.string.lockdown_enter_passphrase) val inBackoff = lockdownState is LockdownState.UnlockBackoff - val passphraseValid = passphrase.isNotEmpty() && passphrase.encodeToByteArray().size <= MAX_PASSPHRASE_LEN val confirmValid = !isProvisioning || passphrase == confirmPassphrase - val isValid = passphraseValid && confirmValid && !inBackoff + val isValid = isValidLockdownPassphrase(passphrase) && confirmValid && !inBackoff AlertDialog( onDismissRequest = {}, // Non-dismissable @@ -139,89 +116,30 @@ fun LockdownDialog( else -> {} } - OutlinedTextField( + LockdownPassphraseField( value = passphrase, - onValueChange = { if (it.encodeToByteArray().size <= MAX_PASSPHRASE_LEN) passphrase = it }, - label = { Text(stringResource(Res.string.lockdown_passphrase)) }, - singleLine = true, - visualTransformation = - if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - trailingIcon = { - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon( - imageVector = - if (passwordVisible) { - MeshtasticIcons.VisibilityOff - } else { - MeshtasticIcons.Visibility - }, - contentDescription = - stringResource( - if (passwordVisible) { - Res.string.lockdown_hide_passphrase - } else { - Res.string.lockdown_show_passphrase - }, - ), - ) - } - }, - modifier = Modifier.fillMaxWidth(), + onValueChange = { passphrase = it }, + label = stringResource(Res.string.lockdown_passphrase), + passwordVisible = passwordVisible, + onToggleVisibility = { passwordVisible = !passwordVisible }, ) if (isProvisioning) { Spacer(modifier = Modifier.height(SPACING_DP.dp)) - OutlinedTextField( + LockdownConfirmPassphraseField( value = confirmPassphrase, - onValueChange = { - if (it.encodeToByteArray().size <= MAX_PASSPHRASE_LEN) confirmPassphrase = it - }, - label = { Text(stringResource(Res.string.lockdown_confirm_passphrase)) }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - isError = confirmPassphrase.isNotEmpty() && passphrase != confirmPassphrase, - supportingText = - if (confirmPassphrase.isNotEmpty() && passphrase != confirmPassphrase) { - { Text(stringResource(Res.string.lockdown_passphrases_do_not_match)) } - } else { - null - }, - modifier = Modifier.fillMaxWidth(), + onValueChange = { confirmPassphrase = it }, + matches = passphrase == confirmPassphrase, ) } Spacer(modifier = Modifier.height(SPACING_DP.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OutlinedTextField( - value = boots.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { boots = it.coerceIn(1, MAX_BYTE_VALUE) } }, - label = { Text(stringResource(Res.string.lockdown_boots_remaining)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(SPACING_DP.dp)) - OutlinedTextField( - value = hours.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { hours = it.coerceAtLeast(0) } }, - label = { Text(stringResource(Res.string.lockdown_hours_until_expiry)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f), - ) - } - Spacer(modifier = Modifier.height(SPACING_DP.dp)) - OutlinedTextField( - value = sessionMinutes.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { sessionMinutes = it.coerceAtLeast(0) } }, - label = { Text(stringResource(Res.string.lockdown_session_minutes)) }, - supportingText = { Text(stringResource(Res.string.lockdown_session_minutes_help)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), + LockdownLimitsFields( + boots = boots, + onBootsChange = { boots = it }, + hours = hours, + onHoursChange = { hours = it }, + sessionMinutes = sessionMinutes, + onSessionMinutesChange = { sessionMinutes = it }, ) } }, @@ -234,7 +152,4 @@ fun LockdownDialog( ) } -// Firmware maximum: AdminMessage.lockdown_auth.passphrase is limited to 64 bytes. -private const val MAX_PASSPHRASE_LEN = 64 -private const val MAX_BYTE_VALUE = 255 private const val SPACING_DP = 8 diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownFields.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownFields.kt new file mode 100644 index 0000000000..2b3ccc5af1 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownFields.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.lockdown + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.lockdown_boots_remaining +import org.meshtastic.core.resources.lockdown_confirm_passphrase +import org.meshtastic.core.resources.lockdown_hide_passphrase +import org.meshtastic.core.resources.lockdown_hours_until_expiry +import org.meshtastic.core.resources.lockdown_passphrases_do_not_match +import org.meshtastic.core.resources.lockdown_session_minutes +import org.meshtastic.core.resources.lockdown_session_minutes_help +import org.meshtastic.core.resources.lockdown_show_passphrase +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Visibility +import org.meshtastic.core.ui.icon.VisibilityOff + +// Firmware maximum: AdminMessage.lockdown_auth.passphrase is limited to 64 bytes. +private const val MAX_PASSPHRASE_LEN = 64 +private const val MAX_BYTE_VALUE = 255 +private const val SPACING_DP = 8 + +/** A passphrase the firmware accepts: non-empty and within its byte limit. */ +internal fun isValidLockdownPassphrase(passphrase: String): Boolean = + passphrase.isNotEmpty() && passphrase.encodeToByteArray().size <= MAX_PASSPHRASE_LEN + +private fun fitsPassphraseLimit(text: String): Boolean = text.encodeToByteArray().size <= MAX_PASSPHRASE_LEN + +@Composable +internal fun LockdownPassphraseField( + value: String, + onValueChange: (String) -> Unit, + label: String, + passwordVisible: Boolean, + onToggleVisibility: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = value, + onValueChange = { if (fitsPassphraseLimit(it)) onValueChange(it) }, + label = { Text(label) }, + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = onToggleVisibility) { + Icon( + imageVector = if (passwordVisible) MeshtasticIcons.VisibilityOff else MeshtasticIcons.Visibility, + contentDescription = + stringResource( + if (passwordVisible) { + Res.string.lockdown_hide_passphrase + } else { + Res.string.lockdown_show_passphrase + }, + ), + ) + } + }, + modifier = modifier.fillMaxWidth(), + ) +} + +/** The confirm-passphrase field; flags a mismatch once anything has been typed. */ +@Composable +internal fun LockdownConfirmPassphraseField( + value: String, + onValueChange: (String) -> Unit, + matches: Boolean, + modifier: Modifier = Modifier, +) { + val showMismatch = value.isNotEmpty() && !matches + OutlinedTextField( + value = value, + onValueChange = { if (fitsPassphraseLimit(it)) onValueChange(it) }, + label = { Text(stringResource(Res.string.lockdown_confirm_passphrase)) }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + isError = showMismatch, + supportingText = + if (showMismatch) { + { Text(stringResource(Res.string.lockdown_passphrases_do_not_match)) } + } else { + null + }, + modifier = modifier.fillMaxWidth(), + ) +} + +/** Boots, hours and session-minute limits for a new lockdown passphrase, clamped to what firmware stores. */ +@Composable +internal fun LockdownLimitsFields( + boots: Int, + onBootsChange: (Int) -> Unit, + hours: Int, + onHoursChange: (Int) -> Unit, + sessionMinutes: Int, + onSessionMinutesChange: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + OutlinedTextField( + value = boots.toString(), + onValueChange = { str -> str.toIntOrNull()?.let { onBootsChange(it.coerceIn(1, MAX_BYTE_VALUE)) } }, + label = { Text(stringResource(Res.string.lockdown_boots_remaining)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(SPACING_DP.dp)) + OutlinedTextField( + value = hours.toString(), + onValueChange = { str -> str.toIntOrNull()?.let { onHoursChange(it.coerceAtLeast(0)) } }, + label = { Text(stringResource(Res.string.lockdown_hours_until_expiry)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f), + ) + } + Spacer(modifier = Modifier.height(SPACING_DP.dp)) + OutlinedTextField( + value = sessionMinutes.toString(), + onValueChange = { str -> str.toIntOrNull()?.let { onSessionMinutesChange(it.coerceAtLeast(0)) } }, + label = { Text(stringResource(Res.string.lockdown_session_minutes)) }, + supportingText = { Text(stringResource(Res.string.lockdown_session_minutes_help)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownModeSetting.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownModeSetting.kt index 5ad2b28bb8..f5a7c2aad7 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownModeSetting.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownModeSetting.kt @@ -16,22 +16,15 @@ */ package org.meshtastic.feature.settings.lockdown -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -43,9 +36,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.service.LockdownState @@ -53,15 +43,11 @@ import org.meshtastic.core.model.service.LockdownTokenInfo import org.meshtastic.core.repository.LockdownPassphraseStore import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel -import org.meshtastic.core.resources.lockdown_boots_remaining -import org.meshtastic.core.resources.lockdown_confirm_passphrase import org.meshtastic.core.resources.lockdown_disable import org.meshtastic.core.resources.lockdown_disable_message import org.meshtastic.core.resources.lockdown_enable import org.meshtastic.core.resources.lockdown_enable_ack import org.meshtastic.core.resources.lockdown_enable_warning -import org.meshtastic.core.resources.lockdown_hide_passphrase -import org.meshtastic.core.resources.lockdown_hours_until_expiry import org.meshtastic.core.resources.lockdown_lock_now import org.meshtastic.core.resources.lockdown_mode import org.meshtastic.core.resources.lockdown_mode_setting_up @@ -69,15 +55,8 @@ import org.meshtastic.core.resources.lockdown_mode_summary_locked import org.meshtastic.core.resources.lockdown_mode_summary_off import org.meshtastic.core.resources.lockdown_mode_summary_unlocked import org.meshtastic.core.resources.lockdown_passphrase -import org.meshtastic.core.resources.lockdown_passphrases_do_not_match -import org.meshtastic.core.resources.lockdown_session_minutes -import org.meshtastic.core.resources.lockdown_session_minutes_help import org.meshtastic.core.resources.lockdown_set_passphrase -import org.meshtastic.core.resources.lockdown_show_passphrase import org.meshtastic.core.ui.component.SwitchPreference -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Visibility -import org.meshtastic.core.ui.icon.VisibilityOff import org.meshtastic.feature.settings.radio.component.NodeActionButton /** @@ -169,7 +148,6 @@ fun ColumnScope.LockdownModeSetting( } } -@Suppress("LongMethod") @Composable private fun EnableLockdownDialog( onConfirm: (passphrase: String, boots: Int, hours: Int, sessionMinutes: Int) -> Unit, @@ -183,9 +161,8 @@ private fun EnableLockdownDialog( var sessionMinutes by rememberSaveable { mutableIntStateOf(0) } var acknowledged by rememberSaveable { mutableStateOf(false) } - val passphraseValid = passphrase.isNotEmpty() && passphrase.encodeToByteArray().size <= MAX_PASSPHRASE_LEN val matches = passphrase == confirmPassphrase - val isValid = passphraseValid && matches && acknowledged + val isValid = isValidLockdownPassphrase(passphrase) && matches && acknowledged AlertDialog( onDismissRequest = onDismiss, @@ -197,7 +174,7 @@ private fun EnableLockdownDialog( color = MaterialTheme.colorScheme.onSurfaceVariant, ) Spacer(modifier = Modifier.height(SPACING_DP.dp)) - PassphraseField( + LockdownPassphraseField( value = passphrase, onValueChange = { passphrase = it }, label = stringResource(Res.string.lockdown_passphrase), @@ -205,50 +182,19 @@ private fun EnableLockdownDialog( onToggleVisibility = { passwordVisible = !passwordVisible }, ) Spacer(modifier = Modifier.height(SPACING_DP.dp)) - OutlinedTextField( + LockdownConfirmPassphraseField( value = confirmPassphrase, - onValueChange = { if (it.encodeToByteArray().size <= MAX_PASSPHRASE_LEN) confirmPassphrase = it }, - label = { Text(stringResource(Res.string.lockdown_confirm_passphrase)) }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - isError = confirmPassphrase.isNotEmpty() && !matches, - supportingText = - if (confirmPassphrase.isNotEmpty() && !matches) { - { Text(stringResource(Res.string.lockdown_passphrases_do_not_match)) } - } else { - null - }, - modifier = Modifier.fillMaxWidth(), + onValueChange = { confirmPassphrase = it }, + matches = matches, ) Spacer(modifier = Modifier.height(SPACING_DP.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OutlinedTextField( - value = boots.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { boots = it.coerceIn(1, MAX_BYTE_VALUE) } }, - label = { Text(stringResource(Res.string.lockdown_boots_remaining)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(SPACING_DP.dp)) - OutlinedTextField( - value = hours.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { hours = it.coerceAtLeast(0) } }, - label = { Text(stringResource(Res.string.lockdown_hours_until_expiry)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f), - ) - } - Spacer(modifier = Modifier.height(SPACING_DP.dp)) - OutlinedTextField( - value = sessionMinutes.toString(), - onValueChange = { str -> str.toIntOrNull()?.let { sessionMinutes = it.coerceAtLeast(0) } }, - label = { Text(stringResource(Res.string.lockdown_session_minutes)) }, - supportingText = { Text(stringResource(Res.string.lockdown_session_minutes_help)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), + LockdownLimitsFields( + boots = boots, + onBootsChange = { boots = it }, + hours = hours, + onHoursChange = { hours = it }, + sessionMinutes = sessionMinutes, + onSessionMinutesChange = { sessionMinutes = it }, ) Spacer(modifier = Modifier.height(SPACING_DP.dp)) Row(verticalAlignment = Alignment.CenterVertically) { @@ -270,7 +216,7 @@ private fun EnableLockdownDialog( private fun DisableLockdownDialog(onConfirm: (passphrase: String) -> Unit, onDismiss: () -> Unit) { var passphrase by rememberSaveable { mutableStateOf("") } var passwordVisible by rememberSaveable { mutableStateOf(false) } - val isValid = passphrase.isNotEmpty() && passphrase.encodeToByteArray().size <= MAX_PASSPHRASE_LEN + val isValid = isValidLockdownPassphrase(passphrase) AlertDialog( onDismissRequest = onDismiss, @@ -279,7 +225,7 @@ private fun DisableLockdownDialog(onConfirm: (passphrase: String) -> Unit, onDis Column { Text(stringResource(Res.string.lockdown_disable_message)) Spacer(modifier = Modifier.height(SPACING_DP.dp)) - PassphraseField( + LockdownPassphraseField( value = passphrase, onValueChange = { passphrase = it }, label = stringResource(Res.string.lockdown_passphrase), @@ -297,40 +243,4 @@ private fun DisableLockdownDialog(onConfirm: (passphrase: String) -> Unit, onDis ) } -@Composable -private fun PassphraseField( - value: String, - onValueChange: (String) -> Unit, - label: String, - passwordVisible: Boolean, - onToggleVisibility: () -> Unit, -) { - OutlinedTextField( - value = value, - onValueChange = { if (it.encodeToByteArray().size <= MAX_PASSPHRASE_LEN) onValueChange(it) }, - label = { Text(label) }, - singleLine = true, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = onToggleVisibility) { - Icon( - imageVector = if (passwordVisible) MeshtasticIcons.VisibilityOff else MeshtasticIcons.Visibility, - contentDescription = - stringResource( - if (passwordVisible) { - Res.string.lockdown_hide_passphrase - } else { - Res.string.lockdown_show_passphrase - }, - ), - ) - } - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -// Firmware maximum: AdminMessage.lockdown_auth.passphrase is limited to 64 bytes. -private const val MAX_PASSPHRASE_LEN = 64 -private const val MAX_BYTE_VALUE = 255 private const val SPACING_DP = 8 diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownSessionStatus.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownSessionStatus.kt index 30af59d0b0..6c10f654e0 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownSessionStatus.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownSessionStatus.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.model.service.LockdownTokenInfo +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.lockdown_session_boots_remaining import org.meshtastic.core.resources.lockdown_session_expires @@ -50,7 +51,7 @@ fun LockdownSessionStatus(tokenInfo: LockdownTokenInfo?, modifier: Modifier = Mo text = stringResource( Res.string.lockdown_session_expires, - DateFormatter.formatDateTime(tokenInfo.expiryEpoch * MILLIS_PER_SECOND), + DateFormatter.formatDateTime(tokenInfo.expiryEpoch * TimeConstants.MS_PER_SEC), ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -67,4 +68,3 @@ fun LockdownSessionStatus(tokenInfo: LockdownTokenInfo?, modifier: Modifier = Mo private const val PADDING_DP = 8 private const val PADDING_VERTICAL_DP = 4 -private const val MILLIS_PER_SECOND = 1000L diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt index 8187d0c384..8346b6c8cb 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt @@ -19,6 +19,7 @@ package org.meshtastic.feature.settings.navigation import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.model.Capabilities +import org.meshtastic.core.model.excludes import org.meshtastic.core.navigation.Route import org.meshtastic.core.navigation.SettingsRoute import org.meshtastic.core.resources.Res @@ -53,6 +54,7 @@ import org.meshtastic.core.resources.telemetry import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.ExcludedModules enum class ModuleRoute( val title: StringResource, @@ -159,50 +161,32 @@ enum class ModuleRoute( ), ; - val bitfield: Int + /** The `excluded_modules` bit a node sets when this module is compiled out of its firmware. */ + val excludedAs: ExcludedModules get() = when (this) { - MQTT -> 0x0001 - - SERIAL -> 0x0002 - - EXT_NOTIFICATION -> 0x0004 - - STORE_FORWARD -> 0x0008 - - RANGE_TEST -> 0x0010 - - TELEMETRY -> 0x0020 - - CANNED_MESSAGE -> 0x0040 - - AUDIO -> 0x0080 - - REMOTE_HARDWARE -> 0x0100 - - NEIGHBOR_INFO -> 0x0200 - - AMBIENT_LIGHTING -> 0x0400 - - DETECTION_SENSOR -> 0x0800 - - PAXCOUNTER -> 0x1000 - - // Not excludable yet - TAK -> 0x0000 - - // Not excludable yet - - MESH_BEACON -> 0x0000 // Not excludable yet + MQTT -> ExcludedModules.MQTT_CONFIG + SERIAL -> ExcludedModules.SERIAL_CONFIG + EXT_NOTIFICATION -> ExcludedModules.EXTNOTIF_CONFIG + STORE_FORWARD -> ExcludedModules.STOREFORWARD_CONFIG + RANGE_TEST -> ExcludedModules.RANGETEST_CONFIG + TELEMETRY -> ExcludedModules.TELEMETRY_CONFIG + CANNED_MESSAGE -> ExcludedModules.CANNEDMSG_CONFIG + AUDIO -> ExcludedModules.AUDIO_CONFIG + REMOTE_HARDWARE -> ExcludedModules.REMOTEHARDWARE_CONFIG + NEIGHBOR_INFO -> ExcludedModules.NEIGHBORINFO_CONFIG + AMBIENT_LIGHTING -> ExcludedModules.AMBIENTLIGHTING_CONFIG + DETECTION_SENSOR -> ExcludedModules.DETECTIONSENSOR_CONFIG + PAXCOUNTER -> ExcludedModules.PAXCOUNTER_CONFIG + TAK -> ExcludedModules.TAK_CONFIG + MESH_BEACON -> ExcludedModules.MESHBEACON_CONFIG } companion object { fun filterExcludedFrom(metadata: DeviceMetadata?, role: Config.DeviceConfig.Role?): List { val capabilities = Capabilities(metadata?.firmware_version) return entries.filter { - val excludedModules = metadata?.excluded_modules ?: 0 - val isExcluded = (excludedModules and it.bitfield) != 0 - !isExcluded && it.isSupported(capabilities) && it.isApplicable(role) + !metadata.excludes(it.excludedAs) && it.isSupported(capabilities) && it.isApplicable(role) } } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt index 4d69446998..12c3ac5e2d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt @@ -475,8 +475,8 @@ fun EntryProviderScope.configComposable( val viewModel = radioConfigViewModelProvider(null) // Remote settings need a blocking progress overlay from the first frame. Local settings already have their // connect-time repository snapshot, so their route refresh stays non-blocking and does not flash a 0% overlay. - remember { viewModel.ensureLoadingForRemote().let { true } } - LaunchedEffect(Unit) { viewModel.loadConfigRoute(routeInfo) } + remember(viewModel, routeInfo) { viewModel.ensureLoadingForRemote().let { true } } + LaunchedEffect(viewModel, routeInfo) { viewModel.loadConfigRoute(routeInfo) } content(viewModel) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMerge.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMerge.kt new file mode 100644 index 0000000000..a88f8073f7 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMerge.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.radio + +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig + +/** Replaces the section [config] carries and keeps every other section. */ +internal fun LocalModuleConfig.mergedWith(config: ModuleConfig): LocalModuleConfig = newBuilder() + .also { wb -> + wb.mqtt = config.mqtt ?: mqtt + wb.serial = config.serial ?: serial + wb.external_notification = config.external_notification ?: external_notification + wb.store_forward = config.store_forward ?: store_forward + wb.range_test = config.range_test ?: range_test + wb.telemetry = config.telemetry ?: telemetry + wb.canned_message = config.canned_message ?: canned_message + wb.audio = config.audio ?: audio + wb.remote_hardware = config.remote_hardware ?: remote_hardware + wb.neighbor_info = config.neighbor_info ?: neighbor_info + wb.ambient_lighting = config.ambient_lighting ?: ambient_lighting + wb.detection_sensor = config.detection_sensor ?: detection_sensor + wb.paxcounter = config.paxcounter ?: paxcounter + wb.statusmessage = config.statusmessage ?: statusmessage + wb.traffic_management = config.traffic_management ?: traffic_management + wb.tak = config.tak ?: tak + wb.mesh_beacon = config.mesh_beacon ?: mesh_beacon + } + .build() diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt index 19dcc01ecb..58d9797012 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt @@ -50,11 +50,13 @@ import org.meshtastic.core.resources.ic_restart_alt import org.meshtastic.core.resources.ic_restore import org.meshtastic.core.resources.ic_schedule import org.meshtastic.core.resources.ic_storage +import org.meshtastic.core.resources.ic_system_update import org.meshtastic.core.resources.import_configuration import org.meshtastic.core.resources.message_device_managed import org.meshtastic.core.resources.module_settings import org.meshtastic.core.resources.nodedb_reset import org.meshtastic.core.resources.reboot +import org.meshtastic.core.resources.reboot_into_dfu import org.meshtastic.core.resources.set_time import org.meshtastic.core.resources.shutdown import org.meshtastic.core.resources.tak_server @@ -227,6 +229,7 @@ private fun AdvancedSection(isManaged: Boolean, isOtaCapable: Boolean, enabled: enum class AdminRoute(val icon: DrawableResource, val title: StringResource) { SET_TIME(Res.drawable.ic_schedule, Res.string.set_time), REBOOT(Res.drawable.ic_restart_alt, Res.string.reboot), + REBOOT_DFU(Res.drawable.ic_system_update, Res.string.reboot_into_dfu), SHUTDOWN(Res.drawable.ic_power_settings_new, Res.string.shutdown), FACTORY_RESET(Res.drawable.ic_restore, Res.string.factory_reset), NODEDB_RESET(Res.drawable.ic_storage, Res.string.nodedb_reset), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 54bb2c4fb2..43c0b9faa5 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -64,8 +64,10 @@ import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.Position +import org.meshtastic.core.model.excludes import org.meshtastic.core.model.util.MalformedMeshtasticUrlException import org.meshtastic.core.repository.AnalyticsPrefs +import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FileService import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.LocationRepository @@ -105,6 +107,7 @@ import org.meshtastic.proto.DeviceConnectionStatus import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.DeviceProfile import org.meshtastic.proto.DeviceUIConfig +import org.meshtastic.proto.ExcludedModules import org.meshtastic.proto.FileInfo import org.meshtastic.proto.HamParameters import org.meshtastic.proto.HardwareModel @@ -147,6 +150,7 @@ data class RadioConfigState( val analyticsAvailable: Boolean = true, val analyticsEnabled: Boolean = true, val nodeDbResetPreserveFavorites: Boolean = false, + val canRebootToDfu: Boolean = false, ) @KoinViewModel @@ -156,6 +160,7 @@ open class RadioConfigViewModel( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, private val nodeRepository: NodeRepository, + private val deviceHardwareRepository: DeviceHardwareRepository, private val locationRepository: LocationRepository, private val mapConsentPrefs: MapConsentPrefs, private val analyticsPrefs: AnalyticsPrefs, @@ -209,13 +214,13 @@ open class RadioConfigViewModel( val analyticsAllowedFlow = analyticsPrefs.analyticsAllowed fun toggleAnalyticsAllowed() { - analyticsPrefs.setAnalyticsAllowed(!analyticsPrefs.analyticsAllowed.value) + analyticsPrefs.toggleAnalyticsAllowed() } val homoglyphEncodingEnabledFlow = homoglyphEncodingPrefs.homoglyphEncodingEnabled fun toggleHomoglyphCharactersEncodingEnabled() { - homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value) + homoglyphEncodingPrefs.toggleHomoglyphEncodingEnabled() } /** MQTT proxy connection state for the settings UI. */ @@ -253,6 +258,7 @@ open class RadioConfigViewModel( private val manualChannelBatchJobs = mutableSetOf() private var manualChannelBatchEnqueueing = false private val manualChannelBatchRequestIds = mutableSetOf() + private val dfuRequestIds = mutableSetOf() /** * Run a one-shot reachability/credentials probe against an MQTT broker. Cancels any in-flight probe before starting @@ -261,16 +267,16 @@ open class RadioConfigViewModel( fun probeMqttConnection(address: String, tlsEnabled: Boolean, username: String?, password: String?) { probeJob?.cancel() _mqttProbeStatus.value = MqttProbeStatus.Probing - probeJob = - viewModelScope.launch { - val result = - safeCatching { mqttManager.probe(address, tlsEnabled, username, password) } - .getOrElse { e -> - Logger.w(e) { "MQTT probe threw" } - MqttProbeStatus.Other(message = e.message) - } - _mqttProbeStatus.value = result + probeJob = viewModelScope.launch { + val result = safeCatching { + mqttManager.probe(address, tlsEnabled, username, password) } + .getOrElse { e -> + Logger.w(e) { "MQTT probe threw" } + MqttProbeStatus.Other(message = e.message) + } + _mqttProbeStatus.value = result + } } /** Clear the latest probe result (e.g. when the user edits the address). */ @@ -325,6 +331,19 @@ open class RadioConfigViewModel( } .launchIn(viewModelScope) + _destNode + .map { it?.user?.hw_model?.value } + .distinctUntilChanged() + .flatMapLatest { hwModel -> + if (hwModel == null) { + flowOf(false) + } else { + deviceHardwareRepository.observeDeviceHardware(hwModel).map { it?.isNrf52Arc == true } + } + } + .onEach { canDfu -> _radioConfigState.update { it.copy(canRebootToDfu = canDfu) } } + .launchIn(viewModelScope) + radioConfigRepository.deviceProfileFlow.onEach { _currentDeviceProfile.value = it }.launchIn(viewModelScope) // Derive isLocal from the immutable destNum and the (possibly changing) myNodeInfo. @@ -530,11 +549,13 @@ open class RadioConfigViewModel( } catch (e: CancellationException) { abortManualChannelBatch(batchRequestIds) throw e - } catch (e: Throwable) { + } catch (e: Exception) { abortManualChannelBatch(batchRequestIds) - if (e !is Exception) throw e Logger.w(e) { "Manual channel update failed after enqueue" } e.message?.let(::sendError) ?: sendError(Res.string.unknown_error) + } catch (e: Throwable) { + abortManualChannelBatch(batchRequestIds) + throw e } } } finally { @@ -597,37 +618,10 @@ open class RadioConfigViewModel( } } - @Suppress("CyclomaticComplexMethod") fun setModuleConfig(config: ModuleConfig) { val destNum = destNum ?: destNode.value?.num ?: return safeLaunch(tag = "setModuleConfig") { - _radioConfigState.update { state -> - state.copy( - moduleConfig = - state.moduleConfig - .newBuilder() - .also { wb -> - wb.mqtt = config.mqtt ?: state.moduleConfig.mqtt - wb.serial = config.serial ?: state.moduleConfig.serial - wb.external_notification = - config.external_notification ?: state.moduleConfig.external_notification - wb.store_forward = config.store_forward ?: state.moduleConfig.store_forward - wb.range_test = config.range_test ?: state.moduleConfig.range_test - wb.telemetry = config.telemetry ?: state.moduleConfig.telemetry - wb.canned_message = config.canned_message ?: state.moduleConfig.canned_message - wb.audio = config.audio ?: state.moduleConfig.audio - wb.remote_hardware = config.remote_hardware ?: state.moduleConfig.remote_hardware - wb.neighbor_info = config.neighbor_info ?: state.moduleConfig.neighbor_info - wb.ambient_lighting = config.ambient_lighting ?: state.moduleConfig.ambient_lighting - wb.detection_sensor = config.detection_sensor ?: state.moduleConfig.detection_sensor - wb.paxcounter = config.paxcounter ?: state.moduleConfig.paxcounter - wb.statusmessage = config.statusmessage ?: state.moduleConfig.statusmessage - wb.tak = config.tak ?: state.moduleConfig.tak - wb.mesh_beacon = config.mesh_beacon ?: state.moduleConfig.mesh_beacon - } - .build(), - ) - } + _radioConfigState.update { state -> state.copy(moduleConfig = state.moduleConfig.mergedWith(config)) } expectRestartIfLocal(config.saveRebootBehavior()) radioConfigUseCase.setModuleConfig(destNum, config, onRequestId = ::registerWriteRequestId) } @@ -672,6 +666,16 @@ open class RadioConfigViewModel( trackAdminAction() } + AdminRoute.REBOOT_DFU.name -> + safeLaunch(tag = "rebootToDfu") { + if (isLocal) nodeRestartTracker.expectRestart() + adminActionsUseCase.rebootToDfu(destNum) { packetId -> + registerRequestId(packetId) + dfuRequestIds.add(packetId) + } + trackAdminAction() + } + AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) { if (metadata?.canShutdown != true) { @@ -849,10 +853,12 @@ open class RadioConfigViewModel( safeLaunch(tag = "getOwner") { radioConfigUseCase.getOwner(destNum, onRequestId = ::registerReadRequestId) } - // The status message is edited on the user screen, so it is read with the owner. Gated on the - // capability: firmware without the module never answers the get, leaving the overlay waiting. + // The status message is edited on the user screen, so it is read with the owner. Gated like the + // editor: firmware without the module never answers the get, leaving the overlay waiting. + val metadata = radioConfigState.value.metadata val readsStatusMessage = - Capabilities(radioConfigState.value.metadata?.firmware_version).supportsStatusMessage + Capabilities(metadata?.firmware_version).supportsStatusMessage && + !metadata.excludes(ExcludedModules.STATUSMESSAGE_CONFIG) loadFanOut = ConfigRoute.USER.name to readsStatusMessage if (readsStatusMessage) { safeLaunch(tag = "getStatusMessageConfig") { @@ -1082,6 +1088,7 @@ open class RadioConfigViewModel( if (requestIds.value.contains(packetId)) { // Capture batch membership before removeRequestId drops the last id and empties the batch set. val timedOutBatchRequest = packetId in manualChannelBatchRequestIds + val timedOutDfuRequest = packetId in dfuRequestIds val requestRoute = readRequestRoutes[packetId].orEmpty() val deferredRemoteReadError = deferredRemoteReadErrors[packetId] removeRequestId(packetId) @@ -1092,12 +1099,12 @@ open class RadioConfigViewModel( // A save that reboots the node races the reboot against its ACK; a timeout here during an // expected restart means the reboot won — treat it as the restarting-success, not an error. // A manual channel batch never reboots, so exclude it even inside a stale restart window. - if ( + // nRF52 firmware jumps to its bootloader without acking, so a DFU request only fails loudly. + val restartWon = nodeRestartTracker.restartExpected.value && - !timedOutBatchRequest && - !manualChannelBatchInFlight() && - radioConfigState.value.route.isEmpty() - ) { + !timedOutBatchRequest && + !manualChannelBatchInFlight() + if ((restartWon || timedOutDfuRequest) && radioConfigState.value.route.isEmpty()) { setResponseStateSuccess() } else { deferredRemoteReadError?.let(::sendError) ?: sendError(Res.string.timeout) @@ -1154,6 +1161,7 @@ open class RadioConfigViewModel( readRequestRoutes.remove(packetId) deferredRemoteReadErrors.remove(packetId) manualChannelBatchRequestIds.remove(packetId) + dfuRequestIds.remove(packetId) requestIds.update { it.withoutPacketId(packetId) } } @@ -1165,6 +1173,7 @@ open class RadioConfigViewModel( removeLateRemoteRead(it) } manualChannelBatchRequestIds.removeAll(packetIds) + dfuRequestIds.removeAll(packetIds) requestIds.update { ids -> ids.withoutPacketIds(packetIds) } } @@ -1338,35 +1347,8 @@ open class RadioConfigViewModel( } is RadioResponseResult.ModuleConfigResponse -> { - val response = result.config _radioConfigState.update { state -> - state.copy( - moduleConfig = - state.moduleConfig - .newBuilder() - .also { wb -> - wb.mqtt = response.mqtt ?: state.moduleConfig.mqtt - wb.serial = response.serial ?: state.moduleConfig.serial - wb.external_notification = - response.external_notification ?: state.moduleConfig.external_notification - wb.store_forward = response.store_forward ?: state.moduleConfig.store_forward - wb.range_test = response.range_test ?: state.moduleConfig.range_test - wb.telemetry = response.telemetry ?: state.moduleConfig.telemetry - wb.canned_message = response.canned_message ?: state.moduleConfig.canned_message - wb.audio = response.audio ?: state.moduleConfig.audio - wb.remote_hardware = response.remote_hardware ?: state.moduleConfig.remote_hardware - wb.neighbor_info = response.neighbor_info ?: state.moduleConfig.neighbor_info - wb.ambient_lighting = - response.ambient_lighting ?: state.moduleConfig.ambient_lighting - wb.detection_sensor = - response.detection_sensor ?: state.moduleConfig.detection_sensor - wb.paxcounter = response.paxcounter ?: state.moduleConfig.paxcounter - wb.statusmessage = response.statusmessage ?: state.moduleConfig.statusmessage - wb.tak = response.tak ?: state.moduleConfig.tak - wb.mesh_beacon = response.mesh_beacon ?: state.moduleConfig.mesh_beacon - } - .build(), - ) + state.copy(moduleConfig = state.moduleConfig.mergedWith(result.config)) } if (!isLateRemoteRead) incrementCompleted() } @@ -1532,6 +1514,8 @@ internal fun Config.saveRebootBehavior(): RebootBehavior = when { else -> RebootBehavior.MAY_RESTART } -/** Firmware `AdminModule::handleSetModuleConfig` reboots for every module section except status message. */ +/** + * Firmware `AdminModule::handleSetModuleConfig` reboots for every module section except status message and Mesh Beacon. + */ internal fun ModuleConfig.saveRebootBehavior(): RebootBehavior = - if (statusmessage != null) RebootBehavior.NEVER else RebootBehavior.ALWAYS + if (statusmessage != null || mesh_beacon != null) RebootBehavior.NEVER else RebootBehavior.ALWAYS diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt index dbe3f30ca6..11f1d2439a 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt @@ -28,11 +28,16 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ambient_lighting import org.meshtastic.core.resources.ambient_lighting_config -import org.meshtastic.core.resources.blue -import org.meshtastic.core.resources.current -import org.meshtastic.core.resources.green -import org.meshtastic.core.resources.led_state -import org.meshtastic.core.resources.red +import org.meshtastic.core.resources.schema_ambientlighting_blue +import org.meshtastic.core.resources.schema_ambientlighting_blue_description +import org.meshtastic.core.resources.schema_ambientlighting_current +import org.meshtastic.core.resources.schema_ambientlighting_current_description +import org.meshtastic.core.resources.schema_ambientlighting_green +import org.meshtastic.core.resources.schema_ambientlighting_green_description +import org.meshtastic.core.resources.schema_ambientlighting_led_state +import org.meshtastic.core.resources.schema_ambientlighting_led_state_description +import org.meshtastic.core.resources.schema_ambientlighting_red +import org.meshtastic.core.resources.schema_ambientlighting_red_description import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel @@ -67,7 +72,8 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U item { TitledCard(title = stringResource(Res.string.ambient_lighting_config)) { SwitchPreference( - title = stringResource(Res.string.led_state), + title = stringResource(Res.string.schema_ambientlighting_led_state), + summary = stringResource(Res.string.schema_ambientlighting_led_state_description), checked = formState.value.led_state, enabled = state.connected, onCheckedChange = { @@ -96,7 +102,8 @@ private fun LedColorFields( ) { androidx.compose.foundation.layout.Column { BoundedIntEditTextPreference( - title = stringResource(Res.string.current), + title = stringResource(Res.string.schema_ambientlighting_current), + summary = stringResource(Res.string.schema_ambientlighting_current_description), value = config.current, metadata = ModuleConfig.AmbientLightingConfig.current, enabled = enabled, @@ -104,7 +111,8 @@ private fun LedColorFields( onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.current = it }.build()) }, ) BoundedIntEditTextPreference( - title = stringResource(Res.string.red), + title = stringResource(Res.string.schema_ambientlighting_red), + summary = stringResource(Res.string.schema_ambientlighting_red_description), value = config.red, metadata = ModuleConfig.AmbientLightingConfig.red, enabled = enabled, @@ -112,7 +120,8 @@ private fun LedColorFields( onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.red = it }.build()) }, ) BoundedIntEditTextPreference( - title = stringResource(Res.string.green), + title = stringResource(Res.string.schema_ambientlighting_green), + summary = stringResource(Res.string.schema_ambientlighting_green_description), value = config.green, metadata = ModuleConfig.AmbientLightingConfig.green, enabled = enabled, @@ -120,7 +129,8 @@ private fun LedColorFields( onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.green = it }.build()) }, ) BoundedIntEditTextPreference( - title = stringResource(Res.string.blue), + title = stringResource(Res.string.schema_ambientlighting_blue), + summary = stringResource(Res.string.schema_ambientlighting_blue_description), value = config.blue, metadata = ModuleConfig.AmbientLightingConfig.blue, enabled = enabled, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt index f519bd2844..21a7ae0c43 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt @@ -27,13 +27,16 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.audio import org.meshtastic.core.resources.audio_config -import org.meshtastic.core.resources.codec2_sample_rate -import org.meshtastic.core.resources.codec_2_enabled -import org.meshtastic.core.resources.i2s_clock -import org.meshtastic.core.resources.i2s_data_in -import org.meshtastic.core.resources.i2s_data_out -import org.meshtastic.core.resources.i2s_word_select -import org.meshtastic.core.resources.ptt_pin +import org.meshtastic.core.resources.schema_audio_bitrate +import org.meshtastic.core.resources.schema_audio_bitrate_description +import org.meshtastic.core.resources.schema_audio_codec2_enabled +import org.meshtastic.core.resources.schema_audio_codec2_enabled_description +import org.meshtastic.core.resources.schema_audio_i2s_din +import org.meshtastic.core.resources.schema_audio_i2s_sck +import org.meshtastic.core.resources.schema_audio_i2s_sd +import org.meshtastic.core.resources.schema_audio_i2s_ws +import org.meshtastic.core.resources.schema_audio_ptt_pin +import org.meshtastic.core.resources.schema_audio_ptt_pin_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -65,7 +68,8 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.audio_config)) { SwitchPreference( - title = stringResource(Res.string.codec_2_enabled), + title = stringResource(Res.string.schema_audio_codec2_enabled), + summary = stringResource(Res.string.schema_audio_codec2_enabled_description), checked = formState.value.codec2_enabled, enabled = state.connected, onCheckedChange = { @@ -75,7 +79,8 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.ptt_pin), + title = stringResource(Res.string.schema_audio_ptt_pin), + summary = stringResource(Res.string.schema_audio_ptt_pin_description), value = formState.value.ptt_pin, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -84,9 +89,9 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { }, ) DropDownPreference( - title = stringResource(Res.string.codec2_sample_rate), + title = stringResource(Res.string.schema_audio_bitrate), + summary = stringResource(Res.string.schema_audio_bitrate_description), enabled = state.connected, - items = ModuleConfig.AudioConfig.Audio_Baud.entries.map { it to it.name }, selectedItem = formState.value.bitrate, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.bitrate = it }.build() @@ -94,7 +99,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.i2s_word_select), + title = stringResource(Res.string.schema_audio_i2s_ws), value = formState.value.i2s_ws, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -103,7 +108,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { }, ) EditTextPreference( - title = stringResource(Res.string.i2s_data_in), + title = stringResource(Res.string.schema_audio_i2s_sd), value = formState.value.i2s_sd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -112,7 +117,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { }, ) EditTextPreference( - title = stringResource(Res.string.i2s_data_out), + title = stringResource(Res.string.schema_audio_i2s_din), value = formState.value.i2s_din, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -121,7 +126,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { }, ) EditTextPreference( - title = stringResource(Res.string.i2s_clock), + title = stringResource(Res.string.schema_audio_i2s_sck), value = formState.value.i2s_sck, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt index 228dd2ec82..d34541dbd0 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt @@ -33,9 +33,11 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.bluetooth import org.meshtastic.core.resources.bluetooth_config -import org.meshtastic.core.resources.bluetooth_enabled -import org.meshtastic.core.resources.fixed_pin -import org.meshtastic.core.resources.pairing_mode +import org.meshtastic.core.resources.schema_bluetooth_enabled +import org.meshtastic.core.resources.schema_bluetooth_enabled_description +import org.meshtastic.core.resources.schema_bluetooth_fixed_pin +import org.meshtastic.core.resources.schema_bluetooth_mode +import org.meshtastic.core.resources.schema_bluetooth_mode_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -69,7 +71,8 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.bluetooth_config)) { SwitchPreference( - title = stringResource(Res.string.bluetooth_enabled), + title = stringResource(Res.string.schema_bluetooth_enabled), + summary = stringResource(Res.string.schema_bluetooth_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -79,7 +82,8 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.pairing_mode), + title = stringResource(Res.string.schema_bluetooth_mode), + summary = stringResource(Res.string.schema_bluetooth_mode_description), enabled = state.connected, items = Config.BluetoothConfig.PairingMode.entries @@ -116,7 +120,7 @@ private fun FixedPinPreference( var pinState by remember(pinValue) { mutableStateOf(pinValue.toString().padStart(PIN_LENGTH, '0')) } val pinIsError = pinState.length != PIN_LENGTH || !pinState.all { it.isDigit() } EditTextPreference( - title = stringResource(Res.string.fixed_pin), + title = stringResource(Res.string.schema_bluetooth_fixed_pin), value = pinState, enabled = enabled, isError = pinIsError, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt index ca7661f264..7ebf161a47 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalFocusManager @@ -30,21 +31,31 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Capabilities import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.allow_input_source import org.meshtastic.core.resources.canned_message import org.meshtastic.core.resources.canned_message_config import org.meshtastic.core.resources.canned_message_enabled -import org.meshtastic.core.resources.generate_input_event_on_ccw -import org.meshtastic.core.resources.generate_input_event_on_cw -import org.meshtastic.core.resources.generate_input_event_on_press -import org.meshtastic.core.resources.gpio_pin_for_rotary_encoder_a_port -import org.meshtastic.core.resources.gpio_pin_for_rotary_encoder_b_port -import org.meshtastic.core.resources.gpio_pin_for_rotary_encoder_press_port import org.meshtastic.core.resources.messages -import org.meshtastic.core.resources.rotary_encoder_1_enabled -import org.meshtastic.core.resources.send_bell -import org.meshtastic.core.resources.up_down_select_input_enabled +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_ccw +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_ccw_description +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_cw +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_cw_description +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_press +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_event_press_description +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_a +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_a_description +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_b +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_b_description +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_press +import org.meshtastic.core.resources.schema_cannedmessage_inputbroker_pin_press_description +import org.meshtastic.core.resources.schema_cannedmessage_rotary1_enabled +import org.meshtastic.core.resources.schema_cannedmessage_rotary1_enabled_description +import org.meshtastic.core.resources.schema_cannedmessage_send_bell +import org.meshtastic.core.resources.schema_cannedmessage_send_bell_description +import org.meshtastic.core.resources.schema_cannedmessage_updown1_enabled +import org.meshtastic.core.resources.schema_cannedmessage_updown1_enabled_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -52,11 +63,15 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.allow_input_source +import org.meshtastic.proto.enabled @Suppress("DEPRECATION", "LongMethod") @Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val firmwareVersion = state.metadata?.firmware_version + val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig.Builder().build() val messages = state.cannedMessageMessages val formState = rememberConfigState(initialValue = cannedMessageConfig) @@ -85,18 +100,21 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni ) { item { TitledCard(title = stringResource(Res.string.canned_message_config)) { + if (capabilities.offers(ModuleConfig.CannedMessageConfig.enabled, isSet = formState.value.enabled)) { + SwitchPreference( + title = stringResource(Res.string.canned_message_enabled), + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() + } SwitchPreference( - title = stringResource(Res.string.canned_message_enabled), - checked = formState.value.enabled, - enabled = state.connected, - onCheckedChange = { - formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() - }, - containerColor = CardDefaults.cardColors().containerColor, - ) - HorizontalDivider() - SwitchPreference( - title = stringResource(Res.string.rotary_encoder_1_enabled), + title = stringResource(Res.string.schema_cannedmessage_rotary1_enabled), + summary = stringResource(Res.string.schema_cannedmessage_rotary1_enabled_description), checked = formState.value.rotary1_enabled, enabled = state.connected, onCheckedChange = { @@ -106,7 +124,8 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.gpio_pin_for_rotary_encoder_a_port), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_a), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_a_description), value = formState.value.inputbroker_pin_a, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -115,7 +134,8 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni }, ) EditTextPreference( - title = stringResource(Res.string.gpio_pin_for_rotary_encoder_b_port), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_b), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_b_description), value = formState.value.inputbroker_pin_b, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -124,7 +144,8 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni }, ) EditTextPreference( - title = stringResource(Res.string.gpio_pin_for_rotary_encoder_press_port), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_press), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_pin_press_description), value = formState.value.inputbroker_pin_press, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -134,9 +155,9 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni }, ) DropDownPreference( - title = stringResource(Res.string.generate_input_event_on_press), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_press), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_event_press_description), enabled = state.connected, - items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_press, onItemSelected = { formState.value = @@ -145,9 +166,9 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.generate_input_event_on_cw), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_cw), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_event_cw_description), enabled = state.connected, - items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_cw, onItemSelected = { formState.value = @@ -156,9 +177,9 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.generate_input_event_on_ccw), + title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_ccw), + summary = stringResource(Res.string.schema_cannedmessage_inputbroker_event_ccw_description), enabled = state.connected, - items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_ccw, onItemSelected = { formState.value = @@ -167,7 +188,8 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.up_down_select_input_enabled), + title = stringResource(Res.string.schema_cannedmessage_updown1_enabled), + summary = stringResource(Res.string.schema_cannedmessage_updown1_enabled_description), checked = formState.value.updown1_enabled, enabled = state.connected, onCheckedChange = { @@ -176,21 +198,30 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() - EditTextPreference( - title = stringResource(Res.string.allow_input_source), - value = formState.value.allow_input_source, - maxSize = 63, // allow_input_source max_size:16 - enabled = state.connected, - isError = false, - keyboardOptions = - KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - formState.value = formState.value.newBuilder().also { wb -> wb.allow_input_source = it }.build() - }, - ) + if ( + capabilities.offers( + ModuleConfig.CannedMessageConfig.allow_input_source, + isSet = formState.value.allow_input_source.isNotEmpty(), + ) + ) { + EditTextPreference( + title = stringResource(Res.string.allow_input_source), + value = formState.value.allow_input_source, + maxSize = 63, // allow_input_source max_size:16 + enabled = state.connected, + isError = false, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.allow_input_source = it }.build() + }, + ) + } SwitchPreference( - title = stringResource(Res.string.send_bell), + title = stringResource(Res.string.schema_cannedmessage_send_bell), + summary = stringResource(Res.string.schema_cannedmessage_send_bell_description), checked = formState.value.send_bell, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt index 36ffc06fba..eba7582113 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt @@ -31,14 +31,22 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.detection_sensor import org.meshtastic.core.resources.detection_sensor_config -import org.meshtastic.core.resources.detection_sensor_enabled -import org.meshtastic.core.resources.detection_trigger_type -import org.meshtastic.core.resources.friendly_name -import org.meshtastic.core.resources.gpio_pin_to_monitor -import org.meshtastic.core.resources.minimum_broadcast_seconds -import org.meshtastic.core.resources.send_bell_with_alert_message -import org.meshtastic.core.resources.state_broadcast_seconds -import org.meshtastic.core.resources.use_input_pullup_mode +import org.meshtastic.core.resources.schema_detectionsensor_detection_trigger_type +import org.meshtastic.core.resources.schema_detectionsensor_detection_trigger_type_description +import org.meshtastic.core.resources.schema_detectionsensor_enabled +import org.meshtastic.core.resources.schema_detectionsensor_enabled_description +import org.meshtastic.core.resources.schema_detectionsensor_minimum_broadcast_secs +import org.meshtastic.core.resources.schema_detectionsensor_minimum_broadcast_secs_description +import org.meshtastic.core.resources.schema_detectionsensor_monitor_pin +import org.meshtastic.core.resources.schema_detectionsensor_monitor_pin_description +import org.meshtastic.core.resources.schema_detectionsensor_name +import org.meshtastic.core.resources.schema_detectionsensor_name_description +import org.meshtastic.core.resources.schema_detectionsensor_send_bell +import org.meshtastic.core.resources.schema_detectionsensor_send_bell_description +import org.meshtastic.core.resources.schema_detectionsensor_state_broadcast_secs +import org.meshtastic.core.resources.schema_detectionsensor_state_broadcast_secs_description +import org.meshtastic.core.resources.schema_detectionsensor_use_pullup +import org.meshtastic.core.resources.schema_detectionsensor_use_pullup_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -74,7 +82,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U item { TitledCard(title = stringResource(Res.string.detection_sensor_config)) { SwitchPreference( - title = stringResource(Res.string.detection_sensor_enabled), + title = stringResource(Res.string.schema_detectionsensor_enabled), + summary = stringResource(Res.string.schema_detectionsensor_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -87,7 +96,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U IntervalConfiguration.DETECTION_SENSOR_MINIMUM.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.minimum_broadcast_seconds), + title = stringResource(Res.string.schema_detectionsensor_minimum_broadcast_secs), + summary = stringResource(Res.string.schema_detectionsensor_minimum_broadcast_secs_description), selectedItem = formState.value.minimum_broadcast_secs.toLong(), enabled = state.connected, items = minimumBroadcastIntervals.map { it.value to it.toDisplayString() }, @@ -99,7 +109,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U val stateBroadcastIntervals = remember { IntervalConfiguration.DETECTION_SENSOR_STATE.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.state_broadcast_seconds), + title = stringResource(Res.string.schema_detectionsensor_state_broadcast_secs), + summary = stringResource(Res.string.schema_detectionsensor_state_broadcast_secs_description), selectedItem = formState.value.state_broadcast_secs.toLong(), enabled = state.connected, items = stateBroadcastIntervals.map { it.value to it.toDisplayString() }, @@ -110,7 +121,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.send_bell_with_alert_message), + title = stringResource(Res.string.schema_detectionsensor_send_bell), + summary = stringResource(Res.string.schema_detectionsensor_send_bell_description), checked = formState.value.send_bell, enabled = state.connected, onCheckedChange = { @@ -120,7 +132,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.friendly_name), + title = stringResource(Res.string.schema_detectionsensor_name), + summary = stringResource(Res.string.schema_detectionsensor_name_description), value = formState.value.name, maxSize = 19, // name max_size:20 enabled = state.connected, @@ -135,7 +148,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U HorizontalDivider() val pins = remember { gpioPins } DropDownPreference( - title = stringResource(Res.string.gpio_pin_to_monitor), + title = stringResource(Res.string.schema_detectionsensor_monitor_pin), + summary = stringResource(Res.string.schema_detectionsensor_monitor_pin_description), items = pins, selectedItem = formState.value.monitor_pin, enabled = state.connected, @@ -145,9 +159,9 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.detection_trigger_type), + title = stringResource(Res.string.schema_detectionsensor_detection_trigger_type), + summary = stringResource(Res.string.schema_detectionsensor_detection_trigger_type_description), enabled = state.connected, - items = ModuleConfig.DetectionSensorConfig.TriggerType.entries.map { it to it.name }, selectedItem = formState.value.detection_trigger_type, onItemSelected = { formState.value = @@ -156,7 +170,8 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.use_input_pullup_mode), + title = stringResource(Res.string.schema_detectionsensor_use_pullup), + summary = stringResource(Res.string.schema_detectionsensor_use_pullup_description), checked = formState.value.use_pullup, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt index 193bd1c32b..9a29bc1c30 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -41,7 +42,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.SpanStyle @@ -50,17 +50,14 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.schemaDescriptionRes import org.meshtastic.core.model.util.isDebug import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.accept import org.meshtastic.core.resources.are_you_sure -import org.meshtastic.core.resources.button_gpio -import org.meshtastic.core.resources.buzzer_gpio import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.clear_time_zone -import org.meshtastic.core.resources.config_device_doubleTapAsButtonPress_summary import org.meshtastic.core.resources.config_device_ledHeartbeatEnabled_summary import org.meshtastic.core.resources.config_device_tripleClickAsAdHocPing_summary import org.meshtastic.core.resources.config_device_tzdef_summary @@ -68,39 +65,26 @@ import org.meshtastic.core.resources.config_device_use_phone_tz import org.meshtastic.core.resources.device import org.meshtastic.core.resources.device_storage_ui_title import org.meshtastic.core.resources.device_theme_language -import org.meshtastic.core.resources.double_tap_as_button_press import org.meshtastic.core.resources.file_entry import org.meshtastic.core.resources.files_available import org.meshtastic.core.resources.gpio import org.meshtastic.core.resources.hardware import org.meshtastic.core.resources.i_know_what_i_m_doing -import org.meshtastic.core.resources.led_heartbeat import org.meshtastic.core.resources.no_files_manifested -import org.meshtastic.core.resources.nodeinfo_broadcast_interval import org.meshtastic.core.resources.options -import org.meshtastic.core.resources.rebroadcast_mode -import org.meshtastic.core.resources.rebroadcast_mode_all_desc -import org.meshtastic.core.resources.rebroadcast_mode_all_skip_decoding_desc -import org.meshtastic.core.resources.rebroadcast_mode_core_portnums_only_desc -import org.meshtastic.core.resources.rebroadcast_mode_known_only_desc -import org.meshtastic.core.resources.rebroadcast_mode_local_only_desc -import org.meshtastic.core.resources.rebroadcast_mode_none_desc -import org.meshtastic.core.resources.role -import org.meshtastic.core.resources.role_client_base_desc -import org.meshtastic.core.resources.role_client_desc -import org.meshtastic.core.resources.role_client_hidden_desc -import org.meshtastic.core.resources.role_client_mute_desc -import org.meshtastic.core.resources.role_lost_and_found_desc -import org.meshtastic.core.resources.role_repeater_desc -import org.meshtastic.core.resources.role_router_client_desc -import org.meshtastic.core.resources.role_router_desc -import org.meshtastic.core.resources.role_router_late_desc -import org.meshtastic.core.resources.role_sensor_desc -import org.meshtastic.core.resources.role_tak_desc -import org.meshtastic.core.resources.role_tak_tracker_desc -import org.meshtastic.core.resources.role_tracker_desc import org.meshtastic.core.resources.router_role_confirmation_text -import org.meshtastic.core.resources.time_zone +import org.meshtastic.core.resources.schema_device_button_gpio +import org.meshtastic.core.resources.schema_device_button_gpio_description +import org.meshtastic.core.resources.schema_device_buzzer_gpio +import org.meshtastic.core.resources.schema_device_buzzer_gpio_description +import org.meshtastic.core.resources.schema_device_double_tap_as_button_press +import org.meshtastic.core.resources.schema_device_double_tap_as_button_press_description +import org.meshtastic.core.resources.schema_device_led_heartbeat_disabled +import org.meshtastic.core.resources.schema_device_node_info_broadcast_secs +import org.meshtastic.core.resources.schema_device_node_info_broadcast_secs_description +import org.meshtastic.core.resources.schema_device_rebroadcast_mode +import org.meshtastic.core.resources.schema_device_role +import org.meshtastic.core.resources.schema_device_tzdef import org.meshtastic.core.resources.triple_click_adhoc_ping import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference @@ -111,6 +95,7 @@ import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.PhoneAndroid import org.meshtastic.core.ui.icon.role +import org.meshtastic.core.ui.theme.link import org.meshtastic.core.ui.util.annotatedStringFromHtml import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.util.IntervalConfiguration @@ -119,42 +104,6 @@ import org.meshtastic.proto.Config @Composable expect fun rememberSystemTimeZonePosixString(): String -@Suppress("DEPRECATION") -private val Config.DeviceConfig.Role.description: StringResource - get() = - when (this) { - Config.DeviceConfig.Role.CLIENT -> Res.string.role_client_desc - Config.DeviceConfig.Role.CLIENT_BASE -> Res.string.role_client_base_desc - Config.DeviceConfig.Role.CLIENT_MUTE -> Res.string.role_client_mute_desc - Config.DeviceConfig.Role.ROUTER -> Res.string.role_router_desc - Config.DeviceConfig.Role.ROUTER_CLIENT -> Res.string.role_router_client_desc - Config.DeviceConfig.Role.REPEATER -> Res.string.role_repeater_desc - Config.DeviceConfig.Role.TRACKER -> Res.string.role_tracker_desc - Config.DeviceConfig.Role.SENSOR -> Res.string.role_sensor_desc - Config.DeviceConfig.Role.TAK -> Res.string.role_tak_desc - Config.DeviceConfig.Role.CLIENT_HIDDEN -> Res.string.role_client_hidden_desc - Config.DeviceConfig.Role.LOST_AND_FOUND -> Res.string.role_lost_and_found_desc - Config.DeviceConfig.Role.TAK_TRACKER -> Res.string.role_tak_tracker_desc - Config.DeviceConfig.Role.ROUTER_LATE -> Res.string.role_router_late_desc - } - -private val Config.DeviceConfig.RebroadcastMode.description: StringResource - get() = - when (this) { - Config.DeviceConfig.RebroadcastMode.ALL -> Res.string.rebroadcast_mode_all_desc - - Config.DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> Res.string.rebroadcast_mode_all_skip_decoding_desc - - Config.DeviceConfig.RebroadcastMode.LOCAL_ONLY -> Res.string.rebroadcast_mode_local_only_desc - - Config.DeviceConfig.RebroadcastMode.KNOWN_ONLY -> Res.string.rebroadcast_mode_known_only_desc - - Config.DeviceConfig.RebroadcastMode.NONE -> Res.string.rebroadcast_mode_none_desc - - Config.DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> - Res.string.rebroadcast_mode_core_portnums_only_desc - } - @Suppress("DEPRECATION", "LongMethod") @Composable fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit) { @@ -193,33 +142,33 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit TitledCard(title = stringResource(Res.string.options)) { val currentRole = formState.value.role DropDownPreference( - title = stringResource(Res.string.role), + title = stringResource(Res.string.schema_device_role), enabled = state.connected, selectedItem = currentRole, onItemSelected = { selectedRole = it }, - summary = stringResource(currentRole.description), + summary = currentRole.schemaDescriptionRes()?.let { stringResource(it) }, itemIcon = { MeshtasticIcons.role(it) }, - itemLabel = { it.name }, ) HorizontalDivider() val currentRebroadcastMode = formState.value.rebroadcast_mode DropDownPreference( - title = stringResource(Res.string.rebroadcast_mode), + title = stringResource(Res.string.schema_device_rebroadcast_mode), enabled = state.connected, selectedItem = currentRebroadcastMode, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.rebroadcast_mode = it }.build() }, - summary = stringResource(currentRebroadcastMode.description), + summary = currentRebroadcastMode.schemaDescriptionRes()?.let { stringResource(it) }, ) HorizontalDivider() val nodeInfoBroadcastIntervals = remember { IntervalConfiguration.NODE_INFO_BROADCAST.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.nodeinfo_broadcast_interval), + title = stringResource(Res.string.schema_device_node_info_broadcast_secs), + summary = stringResource(Res.string.schema_device_node_info_broadcast_secs_description), selectedItem = formState.value.node_info_broadcast_secs.toLong(), enabled = state.connected, items = nodeInfoBroadcastIntervals.map { it.value to it.toDisplayString() }, @@ -234,8 +183,8 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit item { TitledCard(title = stringResource(Res.string.hardware)) { SwitchPreference( - title = stringResource(Res.string.double_tap_as_button_press), - summary = stringResource(Res.string.config_device_doubleTapAsButtonPress_summary), + title = stringResource(Res.string.schema_device_double_tap_as_button_press), + summary = stringResource(Res.string.schema_device_double_tap_as_button_press_description), checked = formState.value.double_tap_as_button_press, enabled = state.connected, onCheckedChange = { @@ -262,7 +211,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit InsetDivider() SwitchPreference( - title = stringResource(Res.string.led_heartbeat), + title = stringResource(Res.string.schema_device_led_heartbeat_disabled), summary = stringResource(Res.string.config_device_ledHeartbeatEnabled_summary), checked = !formState.value.led_heartbeat_disabled, enabled = state.connected, @@ -275,7 +224,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit } } item { - TitledCard(title = stringResource(Res.string.time_zone)) { + TitledCard(title = stringResource(Res.string.schema_device_tzdef)) { val appTzPosixString = rememberSystemTimeZonePosixString() EditTextPreference( @@ -331,7 +280,8 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit item { TitledCard(title = stringResource(Res.string.gpio)) { EditTextPreference( - title = stringResource(Res.string.button_gpio), + title = stringResource(Res.string.schema_device_button_gpio), + summary = stringResource(Res.string.schema_device_button_gpio_description), value = formState.value.button_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -343,7 +293,8 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.buzzer_gpio), + title = stringResource(Res.string.schema_device_buzzer_gpio), + summary = stringResource(Res.string.schema_device_buzzer_gpio_description), value = formState.value.buzzer_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -398,7 +349,7 @@ fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { val annotatedDialogText = annotatedStringFromHtml( html = stringResource(Res.string.router_role_confirmation_text), - linkStyles = TextLinkStyles(style = SpanStyle(color = Color.Blue)), + linkStyles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.link)), ) var confirmed by rememberSaveable { mutableStateOf(false) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt index f4b76e63e3..226152dbb3 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt @@ -23,31 +23,33 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Capabilities import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.advanced -import org.meshtastic.core.resources.always_point_north -import org.meshtastic.core.resources.bold_heading -import org.meshtastic.core.resources.carousel_interval -import org.meshtastic.core.resources.compass_orientation -import org.meshtastic.core.resources.config_display_auto_screen_carousel_secs_summary -import org.meshtastic.core.resources.config_display_compass_north_top_summary -import org.meshtastic.core.resources.config_display_displaymode_summary -import org.meshtastic.core.resources.config_display_flip_screen_summary -import org.meshtastic.core.resources.config_display_heading_bold_summary -import org.meshtastic.core.resources.config_display_oled_summary -import org.meshtastic.core.resources.config_display_screen_on_secs_summary -import org.meshtastic.core.resources.config_display_units_summary -import org.meshtastic.core.resources.config_display_wake_on_tap_or_motion_summary import org.meshtastic.core.resources.display import org.meshtastic.core.resources.display_config -import org.meshtastic.core.resources.display_mode -import org.meshtastic.core.resources.display_time_in_12h_format -import org.meshtastic.core.resources.display_units -import org.meshtastic.core.resources.flip_screen -import org.meshtastic.core.resources.oled_type -import org.meshtastic.core.resources.screen_on_for -import org.meshtastic.core.resources.use_12h_format -import org.meshtastic.core.resources.wake_on_tap_or_motion +import org.meshtastic.core.resources.schema_display_auto_screen_carousel_secs +import org.meshtastic.core.resources.schema_display_auto_screen_carousel_secs_description +import org.meshtastic.core.resources.schema_display_compass_north_top +import org.meshtastic.core.resources.schema_display_compass_north_top_description +import org.meshtastic.core.resources.schema_display_compass_orientation +import org.meshtastic.core.resources.schema_display_compass_orientation_description +import org.meshtastic.core.resources.schema_display_displaymode +import org.meshtastic.core.resources.schema_display_displaymode_description +import org.meshtastic.core.resources.schema_display_flip_screen +import org.meshtastic.core.resources.schema_display_flip_screen_description +import org.meshtastic.core.resources.schema_display_heading_bold +import org.meshtastic.core.resources.schema_display_heading_bold_description +import org.meshtastic.core.resources.schema_display_oled +import org.meshtastic.core.resources.schema_display_oled_description +import org.meshtastic.core.resources.schema_display_screen_on_secs +import org.meshtastic.core.resources.schema_display_screen_on_secs_description +import org.meshtastic.core.resources.schema_display_units +import org.meshtastic.core.resources.schema_display_units_description +import org.meshtastic.core.resources.schema_display_use_12h_clock +import org.meshtastic.core.resources.schema_display_use_12h_clock_description +import org.meshtastic.core.resources.schema_display_wake_on_tap_or_motion +import org.meshtastic.core.resources.schema_display_wake_on_tap_or_motion_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard @@ -55,11 +57,15 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.util.IntervalConfiguration import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.Config +import org.meshtastic.proto.compass_north_top +import org.meshtastic.proto.use_12h_clock @Suppress("DEPRECATION", "LongMethod") @Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val firmwareVersion = state.metadata?.firmware_version + val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } val displayConfig = state.radioConfig.display ?: Config.DisplayConfig.Builder().build() val formState = rememberConfigState(initialValue = displayConfig) @@ -77,31 +83,41 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) { item { TitledCard(title = stringResource(Res.string.display_config)) { + if ( + capabilities.offers( + Config.DisplayConfig.compass_north_top, + isSet = formState.value.compass_north_top, + ) + ) { + SwitchPreference( + title = stringResource(Res.string.schema_display_compass_north_top), + summary = stringResource(Res.string.schema_display_compass_north_top_description), + checked = formState.value.compass_north_top, + enabled = state.connected, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.compass_north_top = it }.build() + }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() + } + if (capabilities.offers(Config.DisplayConfig.use_12h_clock)) { + SwitchPreference( + title = stringResource(Res.string.schema_display_use_12h_clock), + summary = stringResource(Res.string.schema_display_use_12h_clock_description), + enabled = state.connected, + checked = formState.value.use_12h_clock, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_12h_clock = it }.build() + }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() + } SwitchPreference( - title = stringResource(Res.string.always_point_north), - summary = stringResource(Res.string.config_display_compass_north_top_summary), - checked = formState.value.compass_north_top, - enabled = state.connected, - onCheckedChange = { - formState.value = formState.value.newBuilder().also { wb -> wb.compass_north_top = it }.build() - }, - containerColor = CardDefaults.cardColors().containerColor, - ) - HorizontalDivider() - SwitchPreference( - title = stringResource(Res.string.use_12h_format), - summary = stringResource(Res.string.display_time_in_12h_format), - enabled = state.connected, - checked = formState.value.use_12h_clock, - onCheckedChange = { - formState.value = formState.value.newBuilder().also { wb -> wb.use_12h_clock = it }.build() - }, - containerColor = CardDefaults.cardColors().containerColor, - ) - HorizontalDivider() - SwitchPreference( - title = stringResource(Res.string.bold_heading), - summary = stringResource(Res.string.config_display_heading_bold_summary), + title = stringResource(Res.string.schema_display_heading_bold), + summary = stringResource(Res.string.schema_display_heading_bold_description), checked = formState.value.heading_bold, enabled = state.connected, onCheckedChange = { @@ -111,10 +127,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.display_units), - summary = stringResource(Res.string.config_display_units_summary), + title = stringResource(Res.string.schema_display_units), + summary = stringResource(Res.string.schema_display_units_description), enabled = state.connected, - items = Config.DisplayConfig.DisplayUnits.entries.map { it to it.name }, selectedItem = formState.value.units, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.units = it }.build() @@ -127,8 +142,8 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val screenOnIntervals = remember { IntervalConfiguration.DISPLAY_SCREEN_ON.allowedIntervals } val carouselIntervals = remember { IntervalConfiguration.DISPLAY_CAROUSEL.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.screen_on_for), - summary = stringResource(Res.string.config_display_screen_on_secs_summary), + title = stringResource(Res.string.schema_display_screen_on_secs), + summary = stringResource(Res.string.schema_display_screen_on_secs_description), enabled = state.connected, items = screenOnIntervals.map { it to it.toDisplayString() }, selectedItem = @@ -141,8 +156,8 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.carousel_interval), - summary = stringResource(Res.string.config_display_auto_screen_carousel_secs_summary), + title = stringResource(Res.string.schema_display_auto_screen_carousel_secs), + summary = stringResource(Res.string.schema_display_auto_screen_carousel_secs_description), enabled = state.connected, items = carouselIntervals.map { it to it.toDisplayString() }, selectedItem = @@ -158,8 +173,8 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.wake_on_tap_or_motion), - summary = stringResource(Res.string.config_display_wake_on_tap_or_motion_summary), + title = stringResource(Res.string.schema_display_wake_on_tap_or_motion), + summary = stringResource(Res.string.schema_display_wake_on_tap_or_motion_description), checked = formState.value.wake_on_tap_or_motion, enabled = state.connected, onCheckedChange = { @@ -170,8 +185,8 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.flip_screen), - summary = stringResource(Res.string.config_display_flip_screen_summary), + title = stringResource(Res.string.schema_display_flip_screen), + summary = stringResource(Res.string.schema_display_flip_screen_description), checked = formState.value.flip_screen, enabled = state.connected, onCheckedChange = { @@ -181,10 +196,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.display_mode), - summary = stringResource(Res.string.config_display_displaymode_summary), + title = stringResource(Res.string.schema_display_displaymode), + summary = stringResource(Res.string.schema_display_displaymode_description), enabled = state.connected, - items = Config.DisplayConfig.DisplayMode.entries.map { it to it.name }, selectedItem = formState.value.displaymode, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.displaymode = it }.build() @@ -192,10 +206,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.oled_type), - summary = stringResource(Res.string.config_display_oled_summary), + title = stringResource(Res.string.schema_display_oled), + summary = stringResource(Res.string.schema_display_oled_description), enabled = state.connected, - items = Config.DisplayConfig.OledType.entries.map { it to it.name }, selectedItem = formState.value.oled, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.oled = it }.build() @@ -203,9 +216,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.compass_orientation), + title = stringResource(Res.string.schema_display_compass_orientation), + summary = stringResource(Res.string.schema_display_compass_orientation_description), enabled = state.connected, - items = Config.DisplayConfig.CompassOrientation.entries.map { it to it.name }, selectedItem = formState.value.compass_orientation, onItemSelected = { formState.value = diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt index 1f6a2b926f..0145e969ca 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt @@ -34,31 +34,47 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.advanced -import org.meshtastic.core.resources.alert_bell_buzzer -import org.meshtastic.core.resources.alert_bell_led -import org.meshtastic.core.resources.alert_bell_vibra -import org.meshtastic.core.resources.alert_message_buzzer -import org.meshtastic.core.resources.alert_message_led -import org.meshtastic.core.resources.alert_message_vibra import org.meshtastic.core.resources.external_notification import org.meshtastic.core.resources.external_notification_config -import org.meshtastic.core.resources.external_notification_enabled -import org.meshtastic.core.resources.nag_timeout_seconds import org.meshtastic.core.resources.notifications_on_alert_bell_receipt import org.meshtastic.core.resources.notifications_on_message_receipt -import org.meshtastic.core.resources.output_buzzer_gpio -import org.meshtastic.core.resources.output_duration_milliseconds -import org.meshtastic.core.resources.output_led_active_high -import org.meshtastic.core.resources.output_led_gpio -import org.meshtastic.core.resources.output_vibra_gpio import org.meshtastic.core.resources.ringtone -import org.meshtastic.core.resources.use_i2s_as_buzzer -import org.meshtastic.core.resources.use_pwm_buzzer +import org.meshtastic.core.resources.schema_externalnotification_active +import org.meshtastic.core.resources.schema_externalnotification_active_description +import org.meshtastic.core.resources.schema_externalnotification_alert_bell +import org.meshtastic.core.resources.schema_externalnotification_alert_bell_buzzer +import org.meshtastic.core.resources.schema_externalnotification_alert_bell_buzzer_description +import org.meshtastic.core.resources.schema_externalnotification_alert_bell_description +import org.meshtastic.core.resources.schema_externalnotification_alert_bell_vibra +import org.meshtastic.core.resources.schema_externalnotification_alert_bell_vibra_description +import org.meshtastic.core.resources.schema_externalnotification_alert_message +import org.meshtastic.core.resources.schema_externalnotification_alert_message_buzzer +import org.meshtastic.core.resources.schema_externalnotification_alert_message_buzzer_description +import org.meshtastic.core.resources.schema_externalnotification_alert_message_description +import org.meshtastic.core.resources.schema_externalnotification_alert_message_vibra +import org.meshtastic.core.resources.schema_externalnotification_alert_message_vibra_description +import org.meshtastic.core.resources.schema_externalnotification_enabled +import org.meshtastic.core.resources.schema_externalnotification_enabled_description +import org.meshtastic.core.resources.schema_externalnotification_nag_timeout +import org.meshtastic.core.resources.schema_externalnotification_nag_timeout_description +import org.meshtastic.core.resources.schema_externalnotification_output +import org.meshtastic.core.resources.schema_externalnotification_output_buzzer +import org.meshtastic.core.resources.schema_externalnotification_output_buzzer_description +import org.meshtastic.core.resources.schema_externalnotification_output_description +import org.meshtastic.core.resources.schema_externalnotification_output_ms +import org.meshtastic.core.resources.schema_externalnotification_output_ms_description +import org.meshtastic.core.resources.schema_externalnotification_output_vibra +import org.meshtastic.core.resources.schema_externalnotification_output_vibra_description +import org.meshtastic.core.resources.schema_externalnotification_use_i2s_as_buzzer +import org.meshtastic.core.resources.schema_externalnotification_use_i2s_as_buzzer_description +import org.meshtastic.core.resources.schema_externalnotification_use_pwm +import org.meshtastic.core.resources.schema_externalnotification_use_pwm_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.feature.settings.util.FixedOutputDurations import org.meshtastic.feature.settings.util.IntervalConfiguration import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig @@ -106,7 +122,8 @@ fun ExternalNotificationConfigScreenCommon( item { TitledCard(title = stringResource(Res.string.external_notification_config)) { SwitchPreference( - title = stringResource(Res.string.external_notification_enabled), + title = stringResource(Res.string.schema_externalnotification_enabled), + summary = stringResource(Res.string.schema_externalnotification_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -120,7 +137,8 @@ fun ExternalNotificationConfigScreenCommon( item { TitledCard(title = stringResource(Res.string.notifications_on_message_receipt)) { SwitchPreference( - title = stringResource(Res.string.alert_message_led), + title = stringResource(Res.string.schema_externalnotification_alert_message), + summary = stringResource(Res.string.schema_externalnotification_alert_message_description), checked = formState.value.alert_message, enabled = state.connected, onCheckedChange = { @@ -130,7 +148,8 @@ fun ExternalNotificationConfigScreenCommon( ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.alert_message_buzzer), + title = stringResource(Res.string.schema_externalnotification_alert_message_buzzer), + summary = stringResource(Res.string.schema_externalnotification_alert_message_buzzer_description), checked = formState.value.alert_message_buzzer, enabled = state.connected, onCheckedChange = { @@ -141,7 +160,8 @@ fun ExternalNotificationConfigScreenCommon( ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.alert_message_vibra), + title = stringResource(Res.string.schema_externalnotification_alert_message_vibra), + summary = stringResource(Res.string.schema_externalnotification_alert_message_vibra_description), checked = formState.value.alert_message_vibra, enabled = state.connected, onCheckedChange = { @@ -156,7 +176,8 @@ fun ExternalNotificationConfigScreenCommon( item { TitledCard(title = stringResource(Res.string.notifications_on_alert_bell_receipt)) { SwitchPreference( - title = stringResource(Res.string.alert_bell_led), + title = stringResource(Res.string.schema_externalnotification_alert_bell), + summary = stringResource(Res.string.schema_externalnotification_alert_bell_description), checked = formState.value.alert_bell, enabled = state.connected, onCheckedChange = { @@ -166,7 +187,8 @@ fun ExternalNotificationConfigScreenCommon( ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.alert_bell_buzzer), + title = stringResource(Res.string.schema_externalnotification_alert_bell_buzzer), + summary = stringResource(Res.string.schema_externalnotification_alert_bell_buzzer_description), checked = formState.value.alert_bell_buzzer, enabled = state.connected, onCheckedChange = { @@ -176,7 +198,8 @@ fun ExternalNotificationConfigScreenCommon( ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.alert_bell_vibra), + title = stringResource(Res.string.schema_externalnotification_alert_bell_vibra), + summary = stringResource(Res.string.schema_externalnotification_alert_bell_vibra_description), checked = formState.value.alert_bell_vibra, enabled = state.connected, onCheckedChange = { @@ -191,7 +214,8 @@ fun ExternalNotificationConfigScreenCommon( TitledCard(title = stringResource(Res.string.advanced)) { val gpio = remember { org.meshtastic.feature.settings.util.gpioPins } DropDownPreference( - title = stringResource(Res.string.output_led_gpio), + title = stringResource(Res.string.schema_externalnotification_output), + summary = stringResource(Res.string.schema_externalnotification_output_description), items = gpio, selectedItem = formState.value.output.toLong(), enabled = state.connected, @@ -202,7 +226,8 @@ fun ExternalNotificationConfigScreenCommon( if (formState.value.output != 0) { HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.output_led_active_high), + title = stringResource(Res.string.schema_externalnotification_active), + summary = stringResource(Res.string.schema_externalnotification_active_description), checked = formState.value.active, enabled = state.connected, onCheckedChange = { @@ -213,7 +238,8 @@ fun ExternalNotificationConfigScreenCommon( } HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.output_buzzer_gpio), + title = stringResource(Res.string.schema_externalnotification_output_buzzer), + summary = stringResource(Res.string.schema_externalnotification_output_buzzer_description), items = gpio, selectedItem = formState.value.output_buzzer.toLong(), enabled = state.connected, @@ -225,7 +251,8 @@ fun ExternalNotificationConfigScreenCommon( if (formState.value.output_buzzer != 0) { HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.use_pwm_buzzer), + title = stringResource(Res.string.schema_externalnotification_use_pwm), + summary = stringResource(Res.string.schema_externalnotification_use_pwm_description), checked = formState.value.use_pwm, enabled = state.connected, onCheckedChange = { @@ -236,7 +263,8 @@ fun ExternalNotificationConfigScreenCommon( } HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.output_vibra_gpio), + title = stringResource(Res.string.schema_externalnotification_output_vibra), + summary = stringResource(Res.string.schema_externalnotification_output_vibra_description), items = gpio, selectedItem = formState.value.output_vibra.toLong(), enabled = state.connected, @@ -246,9 +274,10 @@ fun ExternalNotificationConfigScreenCommon( }, ) HorizontalDivider() - val outputItems = remember { IntervalConfiguration.OUTPUT.allowedIntervals } + val outputItems = remember { FixedOutputDurations.allowed } DropDownPreference( - title = stringResource(Res.string.output_duration_milliseconds), + title = stringResource(Res.string.schema_externalnotification_output_ms), + summary = stringResource(Res.string.schema_externalnotification_output_ms_description), items = outputItems.map { it.value to it.toDisplayString() }, selectedItem = formState.value.output_ms.toLong(), enabled = state.connected, @@ -259,7 +288,8 @@ fun ExternalNotificationConfigScreenCommon( HorizontalDivider() val nagItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.nag_timeout_seconds), + title = stringResource(Res.string.schema_externalnotification_nag_timeout), + summary = stringResource(Res.string.schema_externalnotification_nag_timeout_description), items = nagItems.map { it.value to it.toDisplayString() }, selectedItem = formState.value.nag_timeout.toLong(), enabled = state.connected, @@ -289,7 +319,8 @@ fun ExternalNotificationConfigScreenCommon( ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.use_i2s_as_buzzer), + title = stringResource(Res.string.schema_externalnotification_use_i2s_as_buzzer), + summary = stringResource(Res.string.schema_externalnotification_use_i2s_as_buzzer_description), checked = formState.value.use_i2s_as_buzzer, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt index eacf7a8204..389815225d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt @@ -33,37 +33,49 @@ import org.meshtastic.core.model.ChannelOption import org.meshtastic.core.model.RegionInfo import org.meshtastic.core.model.RegionPresetConstraint import org.meshtastic.core.model.constraintFor +import org.meshtastic.core.model.normalizeCodingRateOverride import org.meshtastic.core.model.numChannels import org.meshtastic.core.model.presetForRegionChange import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.advanced -import org.meshtastic.core.resources.bandwidth import org.meshtastic.core.resources.bandwidth_default import org.meshtastic.core.resources.bandwidth_option_khz import org.meshtastic.core.resources.bandwidth_unsupported import org.meshtastic.core.resources.bandwidth_unsupported_summary -import org.meshtastic.core.resources.coding_rate -import org.meshtastic.core.resources.config_lora_frequency_slot_summary -import org.meshtastic.core.resources.config_lora_hop_limit_summary +import org.meshtastic.core.resources.config_lora_coding_rate_fraction +import org.meshtastic.core.resources.config_lora_coding_rate_override +import org.meshtastic.core.resources.config_lora_coding_rate_override_max_summary +import org.meshtastic.core.resources.config_lora_coding_rate_override_summary +import org.meshtastic.core.resources.config_lora_coding_rate_preset_default import org.meshtastic.core.resources.config_lora_modem_preset_licensed_summary import org.meshtastic.core.resources.config_lora_modem_preset_summary import org.meshtastic.core.resources.config_lora_region_summary -import org.meshtastic.core.resources.frequency_slot -import org.meshtastic.core.resources.hop_limit -import org.meshtastic.core.resources.ignore_mqtt import org.meshtastic.core.resources.lora -import org.meshtastic.core.resources.modem_preset -import org.meshtastic.core.resources.ok_to_mqtt import org.meshtastic.core.resources.options -import org.meshtastic.core.resources.override_duty_cycle -import org.meshtastic.core.resources.override_frequency_mhz -import org.meshtastic.core.resources.pa_fan_disabled -import org.meshtastic.core.resources.region_frequency_plan -import org.meshtastic.core.resources.spread_factor -import org.meshtastic.core.resources.sx126x_rx_boosted_gain -import org.meshtastic.core.resources.tx_enabled -import org.meshtastic.core.resources.tx_power_dbm -import org.meshtastic.core.resources.use_modem_preset +import org.meshtastic.core.resources.schema_lora_bandwidth +import org.meshtastic.core.resources.schema_lora_channel_num +import org.meshtastic.core.resources.schema_lora_channel_num_description +import org.meshtastic.core.resources.schema_lora_coding_rate +import org.meshtastic.core.resources.schema_lora_config_ok_to_mqtt +import org.meshtastic.core.resources.schema_lora_hop_limit +import org.meshtastic.core.resources.schema_lora_hop_limit_description +import org.meshtastic.core.resources.schema_lora_ignore_mqtt +import org.meshtastic.core.resources.schema_lora_ignore_mqtt_description +import org.meshtastic.core.resources.schema_lora_modem_preset +import org.meshtastic.core.resources.schema_lora_override_duty_cycle +import org.meshtastic.core.resources.schema_lora_override_frequency +import org.meshtastic.core.resources.schema_lora_pa_fan_disabled +import org.meshtastic.core.resources.schema_lora_region +import org.meshtastic.core.resources.schema_lora_spread_factor +import org.meshtastic.core.resources.schema_lora_spread_factor_description +import org.meshtastic.core.resources.schema_lora_sx126x_rx_boosted_gain +import org.meshtastic.core.resources.schema_lora_sx126x_rx_boosted_gain_description +import org.meshtastic.core.resources.schema_lora_tx_enabled +import org.meshtastic.core.resources.schema_lora_tx_enabled_description +import org.meshtastic.core.resources.schema_lora_tx_power +import org.meshtastic.core.resources.schema_lora_tx_power_description +import org.meshtastic.core.resources.schema_lora_use_preset +import org.meshtastic.core.resources.schema_lora_use_preset_description import org.meshtastic.core.ui.component.DropDownItem import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference @@ -71,11 +83,14 @@ import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.feature.settings.util.fieldTitle import org.meshtastic.feature.settings.util.intRange import org.meshtastic.proto.Config import org.meshtastic.proto.Config.LoRaConfig.ModemPreset import org.meshtastic.proto.Config.LoRaConfig.RegionCode +import org.meshtastic.proto.bandwidth import org.meshtastic.proto.hop_limit +import org.meshtastic.proto.tx_power private val SPREAD_FACTOR_RANGE = 5..12 private val CODING_RATE_RANGE = 5..8 @@ -136,6 +151,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } val formState = rememberConfigState(initialValue = loraConfig) + val capabilities = remember(state.metadata?.firmware_version) { Capabilities(state.metadata?.firmware_version) } val primaryChannel = remember(formState.value) { Channel(primarySettings, formState.value) } val focusManager = LocalFocusManager.current @@ -157,7 +173,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config.Builder().also { wb -> wb.lora = it }.build() + val config = Config.Builder().also { wb -> wb.lora = it.withCodingRateOverrideFor(capabilities) }.build() viewModel.setConfig(config) }, ) { @@ -167,8 +183,6 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { // instance, so the locally-cached map (from our own handshake) is reused for remote admin too. Gated // on the *target* node's firmware capability (metadata is per-target): pre-2.8 nodes don't get the // map or the new TINY presets, which also keeps older remotes unconstrained. - val capabilities = - remember(state.metadata?.firmware_version) { Capabilities(state.metadata?.firmware_version) } val regionPresetMap = if (capabilities.supportsLoraRegionPresetMap) state.loraRegionPresetMap else null val presetConstraint = remember(regionPresetMap, formState.value.region) { @@ -180,7 +194,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } val presetsGated = presetConstraint?.isGated(state.localIsLicensed) == true DropDownPreference( - title = stringResource(Res.string.region_frequency_plan), + title = stringResource(Res.string.schema_lora_region), summary = stringResource(Res.string.config_lora_region_summary), enabled = state.connected, items = @@ -205,11 +219,13 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { wb.modem_preset = preset } .build() + .withCodingRateOverrideFor(capabilities) }, ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.use_modem_preset), + title = stringResource(Res.string.schema_lora_use_preset), + summary = stringResource(Res.string.schema_lora_use_preset_description), checked = formState.value.use_preset, enabled = state.connected, onCheckedChange = { @@ -228,7 +244,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { buildPresetItems(presetConstraint, presetsGated, selectedPreset, capabilities) } DropDownPreference( - title = stringResource(Res.string.modem_preset), + title = stringResource(Res.string.schema_lora_modem_preset), summary = if (presetsGated) { stringResource(Res.string.config_lora_modem_preset_licensed_summary) @@ -239,9 +255,22 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { items = presetItems, selectedItem = formState.value.modem_preset, onItemSelected = { - formState.value = formState.value.newBuilder().also { wb -> wb.modem_preset = it }.build() + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.modem_preset = it } + .build() + .withCodingRateOverrideFor(capabilities) }, ) + if (capabilities.supportsCodingRateOverride) { + HorizontalDivider() + CodingRateOverridePreference( + config = formState.value, + enabled = state.connected, + onConfigChange = { formState.value = it }, + ) + } } else { ManualModemSettings( config = formState.value, @@ -257,7 +286,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.advanced)) { SwitchPreference( - title = stringResource(Res.string.ignore_mqtt), + title = stringResource(Res.string.schema_lora_ignore_mqtt), + summary = stringResource(Res.string.schema_lora_ignore_mqtt_description), checked = formState.value.ignore_mqtt, enabled = state.connected, onCheckedChange = { @@ -267,7 +297,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.ok_to_mqtt), + title = stringResource(Res.string.schema_lora_config_ok_to_mqtt), checked = formState.value.config_ok_to_mqtt, enabled = state.connected, onCheckedChange = { @@ -277,7 +307,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.tx_enabled), + title = stringResource(Res.string.schema_lora_tx_enabled), + summary = stringResource(Res.string.schema_lora_tx_enabled_description), checked = formState.value.tx_enabled, enabled = state.connected, onCheckedChange = { @@ -287,7 +318,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.override_duty_cycle), + title = stringResource(Res.string.schema_lora_override_duty_cycle), checked = formState.value.override_duty_cycle, enabled = state.connected, onCheckedChange = { @@ -302,8 +333,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { requireNotNull(Config.LoRaConfig.hop_limit.intRange).map { it to it.toString() } } DropDownPreference( - title = stringResource(Res.string.hop_limit), - summary = stringResource(Res.string.config_lora_hop_limit_summary), + title = stringResource(Res.string.schema_lora_hop_limit), + summary = stringResource(Res.string.schema_lora_hop_limit_description), items = hopLimitItems, selectedItem = formState.value.hop_limit, onItemSelected = { @@ -314,8 +345,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() var isFocusedSlot by remember { mutableStateOf(false) } EditTextPreference( - title = stringResource(Res.string.frequency_slot), - summary = stringResource(Res.string.config_lora_frequency_slot_summary), + title = stringResource(Res.string.schema_lora_channel_num), + summary = stringResource(Res.string.schema_lora_channel_num_description), value = if (isFocusedSlot || formState.value.channel_num != 0) { formState.value.channel_num @@ -333,7 +364,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.sx126x_rx_boosted_gain), + title = stringResource(Res.string.schema_lora_sx126x_rx_boosted_gain), + summary = stringResource(Res.string.schema_lora_sx126x_rx_boosted_gain_description), checked = formState.value.sx126x_rx_boosted_gain, enabled = state.connected, onCheckedChange = { @@ -345,7 +377,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() var isFocusedOverride by remember { mutableStateOf(false) } EditTextPreference( - title = stringResource(Res.string.override_frequency_mhz), + title = stringResource(Res.string.schema_lora_override_frequency), value = if (isFocusedOverride || formState.value.override_frequency != 0f) { formState.value.override_frequency @@ -361,7 +393,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SignedIntegerEditTextPreference( - title = stringResource(Res.string.tx_power_dbm), + title = fieldTitle(Res.string.schema_lora_tx_power, Config.LoRaConfig.tx_power), + summary = stringResource(Res.string.schema_lora_tx_power_description), value = formState.value.tx_power, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -372,7 +405,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { if (viewModel.hasPaFan) { HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.pa_fan_disabled), + title = stringResource(Res.string.schema_lora_pa_fan_disabled), checked = formState.value.pa_fan_disabled, enabled = state.connected, onCheckedChange = { @@ -387,6 +420,46 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } } +private fun Config.LoRaConfig.withCodingRateOverrideFor(capabilities: Capabilities): Config.LoRaConfig = + if (capabilities.supportsCodingRateOverride) normalizeCodingRateOverride() else this + +/** + * Raises the preset's coding rate without leaving the preset (design#161). Only rates above the preset's own are + * offered, since firmware ignores anything lower, and "Preset default" is written as 0. + */ +@Composable +private fun CodingRateOverridePreference( + config: Config.LoRaConfig, + enabled: Boolean, + onConfigChange: (Config.LoRaConfig) -> Unit, +) { + val preset = ChannelOption.from(config.modem_preset) ?: return + val atMax = preset.codingRateOverrides.isEmpty() + val presetDefault = + stringResource( + Res.string.config_lora_coding_rate_preset_default, + stringResource(Res.string.config_lora_coding_rate_fraction, preset.codingRate), + ) + val items = + listOf(DropDownItem(value = 0, label = presetDefault)) + + preset.codingRateOverrides.map { rate -> + DropDownItem(value = rate, label = stringResource(Res.string.config_lora_coding_rate_fraction, rate)) + } + DropDownPreference( + title = stringResource(Res.string.config_lora_coding_rate_override), + summary = + if (atMax) { + stringResource(Res.string.config_lora_coding_rate_override_max_summary) + } else { + stringResource(Res.string.config_lora_coding_rate_override_summary) + }, + enabled = enabled && !atMax, + items = items, + selectedItem = preset.codingRateOverride(config.coding_rate), + onItemSelected = { onConfigChange(config.newBuilder().also { wb -> wb.coding_rate = it }.build()) }, + ) +} + @Composable private fun ManualModemSettings( config: Config.LoRaConfig, @@ -405,7 +478,8 @@ private fun ManualModemSettings( ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.spread_factor), + title = stringResource(Res.string.schema_lora_spread_factor), + summary = stringResource(Res.string.schema_lora_spread_factor_description), value = config.spread_factor, enabled = enabled, isError = config.spread_factor !in SPREAD_FACTOR_RANGE, @@ -418,7 +492,7 @@ private fun ManualModemSettings( ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.coding_rate), + title = stringResource(Res.string.schema_lora_coding_rate), value = config.coding_rate, enabled = enabled, isError = config.coding_rate !in CODING_RATE_RANGE, @@ -443,7 +517,7 @@ internal fun LoRaBandwidthPreference( val options = selection.options if (options == null) { EditTextPreference( - title = stringResource(Res.string.bandwidth), + title = fieldTitle(Res.string.schema_lora_bandwidth, Config.LoRaConfig.bandwidth), value = config.bandwidth, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -473,7 +547,7 @@ internal fun LoRaBandwidthPreference( } DropDownPreference( - title = stringResource(Res.string.bandwidth), + title = stringResource(Res.string.schema_lora_bandwidth), summary = if (selection.isValid) { null diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index 34c7762d83..77145e57f8 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -36,28 +36,27 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.MqttConnectionState import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.network.repository.effectiveTlsEnabled import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.address -import org.meshtastic.core.resources.encryption_enabled import org.meshtastic.core.resources.json_output_enabled -import org.meshtastic.core.resources.map_reporting import org.meshtastic.core.resources.mqtt import org.meshtastic.core.resources.mqtt_config -import org.meshtastic.core.resources.mqtt_enabled import org.meshtastic.core.resources.mqtt_probe_dns_failure import org.meshtastic.core.resources.mqtt_probe_other_failure import org.meshtastic.core.resources.mqtt_probe_rejected @@ -76,12 +75,25 @@ import org.meshtastic.core.resources.mqtt_status_disconnected_with_reason import org.meshtastic.core.resources.mqtt_status_inactive import org.meshtastic.core.resources.mqtt_status_reconnecting import org.meshtastic.core.resources.mqtt_status_reconnecting_with_attempt +import org.meshtastic.core.resources.mqtt_status_topics_refused_all +import org.meshtastic.core.resources.mqtt_status_topics_refused_some import org.meshtastic.core.resources.mqtt_test_connection -import org.meshtastic.core.resources.password -import org.meshtastic.core.resources.proxy_to_client_enabled -import org.meshtastic.core.resources.root_topic -import org.meshtastic.core.resources.tls_enabled -import org.meshtastic.core.resources.username +import org.meshtastic.core.resources.schema_mqtt_address +import org.meshtastic.core.resources.schema_mqtt_address_description +import org.meshtastic.core.resources.schema_mqtt_enabled +import org.meshtastic.core.resources.schema_mqtt_enabled_description +import org.meshtastic.core.resources.schema_mqtt_encryption_enabled +import org.meshtastic.core.resources.schema_mqtt_encryption_enabled_description +import org.meshtastic.core.resources.schema_mqtt_map_reporting_enabled +import org.meshtastic.core.resources.schema_mqtt_password +import org.meshtastic.core.resources.schema_mqtt_proxy_to_client_enabled +import org.meshtastic.core.resources.schema_mqtt_proxy_to_client_enabled_description +import org.meshtastic.core.resources.schema_mqtt_root +import org.meshtastic.core.resources.schema_mqtt_root_description +import org.meshtastic.core.resources.schema_mqtt_tls_enabled +import org.meshtastic.core.resources.schema_mqtt_username +import org.meshtastic.core.resources.schema_mqtt_username_description +import org.meshtastic.core.resources.tls_enabled_public_broker_summary import org.meshtastic.core.ui.component.EditPasswordPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -89,13 +101,16 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.json_enabled -// json_enabled is deprecated in the protobuf schema but remains the only toggle for MQTT JSON -// publish/consume, so the settings UI must keep exposing it until the proto provides a replacement. +// json_enabled still drives MQTT JSON on firmware below its deprecated_since; beyond that the schema gate hides it +// unless the node still holds it set, so a stale value stays visible. @Suppress("DEPRECATION") @Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val firmwareVersion = state.metadata?.firmware_version + val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } val destNode by viewModel.destNode.collectAsStateWithLifecycle() val mqttProxyState by viewModel.mqttConnectionState.collectAsStateWithLifecycle() val mqttProxyActive by viewModel.mqttProxyActive.collectAsStateWithLifecycle() @@ -162,7 +177,8 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.mqtt_config)) { SwitchPreference( - title = stringResource(Res.string.mqtt_enabled), + title = stringResource(Res.string.schema_mqtt_enabled), + summary = stringResource(Res.string.schema_mqtt_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -181,7 +197,8 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.username), + title = stringResource(Res.string.schema_mqtt_username), + summary = stringResource(Res.string.schema_mqtt_username_description), value = formState.value.username, maxSize = 63, // username max_size:64 enabled = state.connected, @@ -195,7 +212,7 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditPasswordPreference( - title = stringResource(Res.string.password), + title = stringResource(Res.string.schema_mqtt_password), value = formState.value.password, maxSize = 63, // password max_size:64 enabled = state.connected, @@ -206,7 +223,8 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.encryption_enabled), + title = stringResource(Res.string.schema_mqtt_encryption_enabled), + summary = stringResource(Res.string.schema_mqtt_encryption_enabled_description), checked = formState.value.encryption_enabled, enabled = state.connected, onCheckedChange = { @@ -215,30 +233,30 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() - SwitchPreference( - title = stringResource(Res.string.json_output_enabled), - checked = formState.value.json_enabled, + if (capabilities.offers(ModuleConfig.MQTTConfig.json_enabled, isSet = formState.value.json_enabled)) { + SwitchPreference( + title = stringResource(Res.string.json_output_enabled), + checked = formState.value.json_enabled, + enabled = state.connected, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.json_enabled = it }.build() + }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() + } + MqttTlsPreference( enabled = state.connected, - onCheckedChange = { - formState.value = formState.value.newBuilder().also { wb -> wb.json_enabled = it }.build() - }, - containerColor = CardDefaults.cardColors().containerColor, - ) - HorizontalDivider() - val resolvedAddress = formState.value.address.ifEmpty { "mqtt.meshtastic.org" } - val enforceTls = effectiveTlsEnabled(resolvedAddress, tlsEnabled = false) - SwitchPreference( - title = stringResource(Res.string.tls_enabled), - checked = formState.value.tls_enabled || enforceTls, - enabled = state.connected && !enforceTls, + address = formState.value.address, + tlsEnabled = formState.value.tls_enabled, onCheckedChange = { formState.value = formState.value.newBuilder().also { wb -> wb.tls_enabled = it }.build() }, - containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.root_topic), + title = stringResource(Res.string.schema_mqtt_root), + summary = stringResource(Res.string.schema_mqtt_root_description), value = formState.value.root, maxSize = 31, // root max_size:32 enabled = state.connected, @@ -252,7 +270,8 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.proxy_to_client_enabled), + title = stringResource(Res.string.schema_mqtt_proxy_to_client_enabled), + summary = stringResource(Res.string.schema_mqtt_proxy_to_client_enabled_description), checked = formState.value.proxy_to_client_enabled, enabled = state.connected, onCheckedChange = { @@ -265,7 +284,7 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } item { - TitledCard(title = stringResource(Res.string.map_reporting)) { + TitledCard(title = stringResource(Res.string.schema_mqtt_map_reporting_enabled)) { val mapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings.Builder().build() MapReportingPreference( @@ -304,6 +323,36 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { private const val MIN_INTERVAL_SECS = 3600 +// An empty address means the public broker, both here and in the firmware's PubSubConfig. +private const val DEFAULT_MQTT_ADDRESS = "mqtt.meshtastic.org" + +internal const val MQTT_TLS_SWITCH_TEST_TAG = "mqtt_tls_switch" + +/** + * The radio uses the stored `tls_enabled` verbatim when it reaches the broker over its own Wi-Fi or Ethernet, so what + * this switch shows must be what gets stored and sent: it renders and writes that flag alone, never a forced value. The + * phone-relay's own TLS upgrade for the public broker is stated in the summary instead. + */ +@Composable +internal fun MqttTlsPreference( + enabled: Boolean, + address: String, + tlsEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + val resolvedAddress = address.ifEmpty { DEFAULT_MQTT_ADDRESS } + val relayForcesTls = effectiveTlsEnabled(resolvedAddress, tlsEnabled = false) + SwitchPreference( + title = stringResource(Res.string.schema_mqtt_tls_enabled), + summary = if (relayForcesTls) stringResource(Res.string.tls_enabled_public_broker_summary) else "", + checked = tlsEnabled, + enabled = enabled, + modifier = Modifier.testTag(MQTT_TLS_SWITCH_TEST_TAG), + onCheckedChange = onCheckedChange, + containerColor = CardDefaults.cardColors().containerColor, + ) +} + private val AmberColor = Color(0xFFFFA000) private val GreenColor = Color(0xFF4CAF50) @@ -325,6 +374,15 @@ private fun MqttStatusRow(state: MqttConnectionState) { is MqttConnectionState.Connected -> stringResource(Res.string.mqtt_status_connected) to GreenColor + is MqttConnectionState.SubscriptionRefused -> { + val topics = state.refused.entries.joinToString { (topic, reason) -> "$topic ($reason)" } + if (state.granted == 0) { + stringResource(Res.string.mqtt_status_topics_refused_all, topics) to MaterialTheme.colorScheme.error + } else { + stringResource(Res.string.mqtt_status_topics_refused_some, topics) to AmberColor + } + } + is MqttConnectionState.Reconnecting -> { val err = state.lastError val text = @@ -360,7 +418,8 @@ private fun MqttAddressAndProbe( onClearProbe: () -> Unit, ) { EditTextPreference( - title = stringResource(Res.string.address), + title = stringResource(Res.string.schema_mqtt_address), + summary = stringResource(Res.string.schema_mqtt_address_description), value = formState.value.address, maxSize = 63, // address max_size:64 enabled = enabled, @@ -378,7 +437,7 @@ private fun MqttAddressAndProbe( status = probeStatus, onTestClick = { focusManager.clearFocus() - val resolvedAddress = formState.value.address.ifEmpty { "mqtt.meshtastic.org" } + val resolvedAddress = formState.value.address.ifEmpty { DEFAULT_MQTT_ADDRESS } val effectiveTls = effectiveTlsEnabled(resolvedAddress, formState.value.tls_enabled) onProbe(formState.value.address, effectiveTls, formState.value.username, formState.value.password) }, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt index 8c6e552932..ccf1d59c61 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt @@ -45,11 +45,12 @@ import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.i_agree import org.meshtastic.core.resources.i_agree_to_share_my_location -import org.meshtastic.core.resources.map_reporting import org.meshtastic.core.resources.map_reporting_consent_header import org.meshtastic.core.resources.map_reporting_consent_text -import org.meshtastic.core.resources.map_reporting_interval_seconds -import org.meshtastic.core.resources.map_reporting_summary +import org.meshtastic.core.resources.schema_mapreportsettings_publish_interval_secs +import org.meshtastic.core.resources.schema_mapreportsettings_publish_interval_secs_description +import org.meshtastic.core.resources.schema_mqtt_map_reporting_enabled +import org.meshtastic.core.resources.schema_mqtt_map_reporting_enabled_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.feature.settings.util.FixedUpdateIntervals @@ -78,8 +79,8 @@ fun MapReportingPreference( // [mapReportingEnabled] so an externally-changed config wins over a stale local toggle. var showMapReportingWarning by rememberSaveable(mapReportingEnabled) { mutableStateOf(mapReportingEnabled) } SwitchPreference( - title = stringResource(Res.string.map_reporting), - summary = stringResource(Res.string.map_reporting_summary), + title = stringResource(Res.string.schema_mqtt_map_reporting_enabled), + summary = stringResource(Res.string.schema_mqtt_map_reporting_enabled_description), checked = showMapReportingWarning, enabled = enabled, onCheckedChange = { checked -> @@ -140,7 +141,8 @@ fun MapReportingPreference( val publishItems = remember { IntervalConfiguration.BROADCAST_MEDIUM.allowedIntervals } DropDownPreference( modifier = Modifier.padding(bottom = 16.dp), - title = stringResource(Res.string.map_reporting_interval_seconds), + title = stringResource(Res.string.schema_mapreportsettings_publish_interval_secs), + summary = stringResource(Res.string.schema_mapreportsettings_publish_interval_secs_description), items = publishItems.map { it to it.toDisplayString() }, selectedItem = FixedUpdateIntervals.fromValue(publishIntervalSecs.toLong()) ?: publishItems.first(), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt index 80559fa785..41c14a3795 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt @@ -48,7 +48,6 @@ import org.meshtastic.core.resources.mesh_beacon_interval import org.meshtastic.core.resources.mesh_beacon_interval_error import org.meshtastic.core.resources.mesh_beacon_listen import org.meshtastic.core.resources.mesh_beacon_listen_summary -import org.meshtastic.core.resources.mesh_beacon_message import org.meshtastic.core.resources.mesh_beacon_no_channels import org.meshtastic.core.resources.mesh_beacon_offer_channel_name import org.meshtastic.core.resources.mesh_beacon_on_preset @@ -61,6 +60,7 @@ import org.meshtastic.core.resources.mesh_beacon_target_default import org.meshtastic.core.resources.mesh_beacon_target_remove import org.meshtastic.core.resources.mesh_beacon_targets import org.meshtastic.core.resources.plurals_seconds +import org.meshtastic.core.resources.schema_meshbeacon_broadcast_message import org.meshtastic.core.ui.component.DropDownItem import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference @@ -68,6 +68,7 @@ import org.meshtastic.core.ui.component.RegularPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.feature.settings.util.FixedUpdateIntervals import org.meshtastic.feature.settings.util.IntervalConfiguration import org.meshtastic.feature.settings.util.toDisplayString @@ -78,7 +79,8 @@ import org.meshtastic.proto.Config.LoRaConfig.RegionCode import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.ModuleConfig.MeshBeaconConfig -private const val MESSAGE_MAX_BYTES = 100 +// nanopb's max_size is 61 because it counts the terminator; the string itself gets a round 60. +private const val MESSAGE_MAX_BYTES = 60 private val MIN_INTERVAL_SECS = FixedUpdateIntervals.ONE_HOUR.value.toInt() // Sentinel DropDownItem values (design#140's never-render-blank rule): -1 marks a stale stored value that no longer @@ -180,6 +182,7 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, // broadcast fields verbatim. saveEnabled = meshBeaconSaveEnabled(state.connected, radioLora.use_preset, intervalValid, state.channelList.isNotEmpty()), + rebootBehavior = RebootBehavior.NEVER, responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { @@ -238,7 +241,7 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, if (broadcastGate.sectionsVisible) { HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.mesh_beacon_message), + title = stringResource(Res.string.schema_meshbeacon_broadcast_message), value = formState.value.broadcast_message, maxSize = MESSAGE_MAX_BYTES, enabled = broadcastGate.sectionsEnabled, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt index beb1cbc858..4be5581ef1 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt @@ -18,6 +18,7 @@ package org.meshtastic.feature.settings.radio.component import org.meshtastic.core.model.RegionPresetConstraint import org.meshtastic.core.model.constraintFor +import org.meshtastic.core.model.util.beaconOfferFrequencySlot import org.meshtastic.core.model.util.isChannelPlaceholder import org.meshtastic.feature.settings.util.FixedUpdateIntervals import org.meshtastic.proto.ChannelSettings @@ -81,12 +82,12 @@ internal fun beaconOfferChannelIndex( /** * Applies design#140's save-time invariants to [form] before it is written. When [radioLora] uses a standard modem - * preset (`use_preset = true`): the radio's own region (behavior 1) and configured preset (behavior 4) are always - * stamped, never user-chosen; every broadcast target's region is kept in lockstep; and an untouched offered channel - * defaults to the radio's primary channel (behavior 3's required-channel rule), while an already-set offered channel - * (even one that no longer matches a radio channel) is kept. Only the offered channel's name and PSK are carried over, - * never the radio's own channel_index/id/uplink/downlink/module flags, which have no meaning for a channel someone - * else's radio is being invited to join. + * preset (`use_preset = true`): the radio's own region (behavior 1), configured preset (behavior 4) and frequency slot + * ([beaconOfferFrequencySlot]) are always stamped, never user-chosen; every broadcast target's region is kept in + * lockstep; and an untouched offered channel defaults to the radio's primary channel (behavior 3's required-channel + * rule), while an already-set offered channel (even one that no longer matches a radio channel) is kept. Only the + * offered channel's name and PSK are carried over, never the radio's own channel_index/id/uplink/downlink/module flags, + * which have no meaning for a channel someone else's radio is being invited to join. * * When the radio uses custom LoRa parameters (`use_preset = false`), `modem_preset` is meaningless, so every broadcast * field instead carries over from [stored] verbatim: stamping a stale preset would mint a live on-air lie about what @@ -100,20 +101,23 @@ internal fun stampBeaconConfigForSave( radioLora: Config.LoRaConfig, channelList: List, ): MeshBeaconConfig = if (radioLora.use_preset) { + val offerChannel = + (form.broadcast_offer_channel ?: channelList.getOrNull(0))?.let { + ChannelSettings.Builder() + .also { wb -> + wb.name = it.name + wb.psk = it.psk + } + .build() + } form .newBuilder() .also { wb -> wb.broadcast_offer_region = radioLora.region wb.broadcast_offer_preset = radioLora.modem_preset - wb.broadcast_offer_channel = - (form.broadcast_offer_channel ?: channelList.getOrNull(0))?.let { - ChannelSettings.Builder() - .also { wb -> - wb.name = it.name - wb.psk = it.psk - } - .build() - } + wb.broadcast_offer_channel = offerChannel + wb.broadcast_offer_frequency_slot = + beaconOfferFrequencySlot(offerChannel, channelList.getOrNull(0), radioLora) wb.broadcast_targets = form.broadcast_targets.map { it.newBuilder().also { wb -> wb.region = radioLora.region }.build() } } @@ -127,6 +131,7 @@ internal fun stampBeaconConfigForSave( wb.broadcast_offer_region = stored.broadcast_offer_region wb.broadcast_offer_preset = stored.broadcast_offer_preset wb.broadcast_offer_channel = stored.broadcast_offer_channel + wb.broadcast_offer_frequency_slot = stored.broadcast_offer_frequency_slot wb.broadcast_targets = stored.broadcast_targets } .build() diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt index 5a27efa87d..b52827aad8 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt @@ -25,12 +25,14 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.config_device_transmitOverLora_summary import org.meshtastic.core.resources.neighbor_info import org.meshtastic.core.resources.neighbor_info_config -import org.meshtastic.core.resources.neighbor_info_enabled -import org.meshtastic.core.resources.transmit_over_lora -import org.meshtastic.core.resources.update_interval_seconds +import org.meshtastic.core.resources.schema_neighborinfo_enabled +import org.meshtastic.core.resources.schema_neighborinfo_enabled_description +import org.meshtastic.core.resources.schema_neighborinfo_transmit_over_lora +import org.meshtastic.core.resources.schema_neighborinfo_transmit_over_lora_description +import org.meshtastic.core.resources.schema_neighborinfo_update_interval +import org.meshtastic.core.resources.schema_neighborinfo_update_interval_description import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard @@ -61,7 +63,8 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit item { TitledCard(title = stringResource(Res.string.neighbor_info_config)) { SwitchPreference( - title = stringResource(Res.string.neighbor_info_enabled), + title = stringResource(Res.string.schema_neighborinfo_enabled), + summary = stringResource(Res.string.schema_neighborinfo_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -71,7 +74,8 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.update_interval_seconds), + title = stringResource(Res.string.schema_neighborinfo_update_interval), + summary = stringResource(Res.string.schema_neighborinfo_update_interval_description), value = formState.value.update_interval, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -81,8 +85,8 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.transmit_over_lora), - summary = stringResource(Res.string.config_device_transmitOverLora_summary), + title = stringResource(Res.string.schema_neighborinfo_transmit_over_lora), + summary = stringResource(Res.string.schema_neighborinfo_transmit_over_lora_description), checked = formState.value.transmit_over_lora, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt index 1bc285d3f8..e1f4a7c279 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -39,33 +40,37 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.extractWifiCredentials +import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.util.handleMeshtasticUri import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.advanced import org.meshtastic.core.resources.cancel -import org.meshtastic.core.resources.config_network_eth_enabled_summary import org.meshtastic.core.resources.config_network_udp_enabled_summary -import org.meshtastic.core.resources.config_network_wifi_enabled_summary import org.meshtastic.core.resources.connection_status -import org.meshtastic.core.resources.dns import org.meshtastic.core.resources.error import org.meshtastic.core.resources.ethernet_config -import org.meshtastic.core.resources.ethernet_enabled import org.meshtastic.core.resources.ethernet_ip -import org.meshtastic.core.resources.gateway -import org.meshtastic.core.resources.ipv4_mode import org.meshtastic.core.resources.network import org.meshtastic.core.resources.nfc_disabled -import org.meshtastic.core.resources.ntp_server import org.meshtastic.core.resources.open_settings -import org.meshtastic.core.resources.password -import org.meshtastic.core.resources.rsyslog_server import org.meshtastic.core.resources.scan_nfc -import org.meshtastic.core.resources.ssid -import org.meshtastic.core.resources.subnet +import org.meshtastic.core.resources.schema_network_address_mode +import org.meshtastic.core.resources.schema_network_eth_enabled +import org.meshtastic.core.resources.schema_network_eth_enabled_description +import org.meshtastic.core.resources.schema_network_ipv4_dns +import org.meshtastic.core.resources.schema_network_ipv4_gateway +import org.meshtastic.core.resources.schema_network_ipv4_ip +import org.meshtastic.core.resources.schema_network_ipv4_subnet +import org.meshtastic.core.resources.schema_network_ntp_server +import org.meshtastic.core.resources.schema_network_ntp_server_description +import org.meshtastic.core.resources.schema_network_rsyslog_server +import org.meshtastic.core.resources.schema_network_wifi_enabled +import org.meshtastic.core.resources.schema_network_wifi_enabled_description +import org.meshtastic.core.resources.schema_network_wifi_psk +import org.meshtastic.core.resources.schema_network_wifi_ssid +import org.meshtastic.core.resources.schema_network_wifi_ssid_description import org.meshtastic.core.resources.udp_enabled import org.meshtastic.core.resources.wifi_config -import org.meshtastic.core.resources.wifi_enabled import org.meshtastic.core.resources.wifi_ip import org.meshtastic.core.resources.wifi_qr_code_error import org.meshtastic.core.resources.wifi_qr_code_scan @@ -83,6 +88,7 @@ import org.meshtastic.core.ui.util.LocalNfcScannerSupported import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.proto.Config +import org.meshtastic.proto.enabled_protocols @Composable private fun ScanErrorDialog(onDismiss: () -> Unit = {}) = @@ -98,6 +104,8 @@ private fun formatIpAddress(ipAddress: Int): String = "${(ipAddress) and 0xFF}." @Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onOpenNfcSettings: () -> Unit = {}) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val firmwareVersion = state.metadata?.firmware_version + val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } val networkConfig = state.radioConfig.network ?: Config.NetworkConfig.Builder().build() val formState = rememberConfigState(initialValue = networkConfig) @@ -201,8 +209,8 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO item { TitledCard(title = stringResource(Res.string.wifi_config)) { SwitchPreference( - title = stringResource(Res.string.wifi_enabled), - summary = stringResource(Res.string.config_network_wifi_enabled_summary), + title = stringResource(Res.string.schema_network_wifi_enabled), + summary = stringResource(Res.string.schema_network_wifi_enabled_description), checked = formState.value.wifi_enabled, onCheckedChange = { formState.value = formState.value.newBuilder().also { wb -> wb.wifi_enabled = it }.build() @@ -212,7 +220,8 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO if (formState.value.wifi_enabled) { HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.ssid), + title = stringResource(Res.string.schema_network_wifi_ssid), + summary = stringResource(Res.string.schema_network_wifi_ssid_description), value = formState.value.wifi_ssid, maxSize = 32, // wifi_ssid max_size:33 enabled = state.connected, @@ -226,7 +235,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO ) HorizontalDivider() EditPasswordPreference( - title = stringResource(Res.string.password), + title = stringResource(Res.string.schema_network_wifi_psk), value = formState.value.wifi_psk, maxSize = 64, // wifi_psk max_size:65 enabled = state.connected, @@ -255,8 +264,8 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO item { TitledCard(title = stringResource(Res.string.ethernet_config)) { SwitchPreference( - title = stringResource(Res.string.ethernet_enabled), - summary = stringResource(Res.string.config_network_eth_enabled_summary), + title = stringResource(Res.string.schema_network_eth_enabled), + summary = stringResource(Res.string.schema_network_eth_enabled_description), checked = formState.value.eth_enabled, onCheckedChange = { formState.value = formState.value.newBuilder().also { wb -> wb.eth_enabled = it }.build() @@ -269,7 +278,8 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO item { TitledCard(title = stringResource(Res.string.advanced)) { EditTextPreference( - title = stringResource(Res.string.ntp_server), + title = stringResource(Res.string.schema_network_ntp_server), + summary = stringResource(Res.string.schema_network_ntp_server_description), value = formState.value.ntp_server, maxSize = 32, // ntp_server max_size:33 enabled = state.connected, @@ -283,7 +293,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.rsyslog_server), + title = stringResource(Res.string.schema_network_rsyslog_server), value = formState.value.rsyslog_server, maxSize = 32, // rsyslog_server max_size:33 enabled = state.connected, @@ -296,41 +306,42 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO }, ) HorizontalDivider() - SwitchPreference( - title = stringResource(Res.string.udp_enabled), - summary = stringResource(Res.string.config_network_udp_enabled_summary), - checked = - formState.value.enabled_protocols and Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value != - 0, - onCheckedChange = { enabled -> - val flags = - if (enabled) { - formState.value.enabled_protocols or - Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value - } else { - formState.value.enabled_protocols and - Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value.inv() - } - formState.value = - formState.value.newBuilder().also { wb -> wb.enabled_protocols = flags }.build() - }, - enabled = state.connected, - ) - HorizontalDivider() + if (capabilities.offers(Config.NetworkConfig.enabled_protocols)) { + SwitchPreference( + title = stringResource(Res.string.udp_enabled), + summary = stringResource(Res.string.config_network_udp_enabled_summary), + checked = + formState.value.enabled_protocols and + Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value != 0, + onCheckedChange = { enabled -> + val flags = + if (enabled) { + formState.value.enabled_protocols or + Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value + } else { + formState.value.enabled_protocols and + Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value.inv() + } + formState.value = + formState.value.newBuilder().also { wb -> wb.enabled_protocols = flags }.build() + }, + enabled = state.connected, + ) + HorizontalDivider() + } DropDownPreference( - title = stringResource(Res.string.ipv4_mode), + title = stringResource(Res.string.schema_network_address_mode), enabled = state.connected, selectedItem = formState.value.address_mode, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.address_mode = it }.build() }, - itemLabel = { it.name }, ) if (formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC) { HorizontalDivider() val ipv4 = formState.value.ipv4_config ?: Config.NetworkConfig.IpV4Config.Builder().build() EditIPv4Preference( - title = stringResource(Res.string.wifi_ip), + title = stringResource(Res.string.schema_network_ipv4_ip), value = ipv4.ip, enabled = state.connected, onValueChanged = { @@ -344,7 +355,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO ) HorizontalDivider() EditIPv4Preference( - title = stringResource(Res.string.gateway), + title = stringResource(Res.string.schema_network_ipv4_gateway), value = ipv4.gateway, enabled = state.connected, onValueChanged = { @@ -360,7 +371,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO ) HorizontalDivider() EditIPv4Preference( - title = stringResource(Res.string.subnet), + title = stringResource(Res.string.schema_network_ipv4_subnet), value = ipv4.subnet, enabled = state.connected, onValueChanged = { @@ -376,7 +387,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO ) HorizontalDivider() EditIPv4Preference( - title = stringResource(Res.string.dns), + title = stringResource(Res.string.schema_network_ipv4_dns), value = ipv4.dns, enabled = state.connected, onValueChanged = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt index 6373990f16..f40dbba483 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign @@ -60,11 +61,13 @@ fun PacketResponseStateDialog( onBack: () -> Unit = {}, rebootBehavior: RebootBehavior = RebootBehavior.MAY_RESTART, ) { + val currentOnDismiss by rememberUpdatedState(onDismiss) + val currentOnBack by rememberUpdatedState(onBack) LaunchedEffect(state) { if (state is ResponseState.Success) { delay(AUTO_DISMISS_DELAY_MS) - onDismiss() - onBack() + currentOnDismiss() + currentOnBack() } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt index 7d8e0b4a90..a9b14f8658 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt @@ -26,12 +26,16 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.ble_rssi_threshold_defaults_to_80 import org.meshtastic.core.resources.paxcounter import org.meshtastic.core.resources.paxcounter_config -import org.meshtastic.core.resources.paxcounter_enabled -import org.meshtastic.core.resources.update_interval_seconds -import org.meshtastic.core.resources.wifi_rssi_threshold_defaults_to_80 +import org.meshtastic.core.resources.schema_paxcounter_ble_threshold +import org.meshtastic.core.resources.schema_paxcounter_ble_threshold_description +import org.meshtastic.core.resources.schema_paxcounter_enabled +import org.meshtastic.core.resources.schema_paxcounter_enabled_description +import org.meshtastic.core.resources.schema_paxcounter_paxcounter_update_interval +import org.meshtastic.core.resources.schema_paxcounter_paxcounter_update_interval_description +import org.meshtastic.core.resources.schema_paxcounter_wifi_threshold +import org.meshtastic.core.resources.schema_paxcounter_wifi_threshold_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -39,15 +43,17 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.feature.settings.util.IntervalConfiguration +import org.meshtastic.feature.settings.util.fieldTitle import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.ble_threshold +import org.meshtastic.proto.wifi_threshold @Composable fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig.Builder().build() val formState = rememberConfigState(initialValue = paxcounterConfig) - val focusManager = LocalFocusManager.current RadioConfigScreenList( rebootBehavior = RebootBehavior.ALWAYS, @@ -62,53 +68,57 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) viewModel.setModuleConfig(config) }, ) { - item { - TitledCard(title = stringResource(Res.string.paxcounter_config)) { - SwitchPreference( - title = stringResource(Res.string.paxcounter_enabled), - checked = formState.value.enabled, - enabled = state.connected, - onCheckedChange = { - formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() - }, - containerColor = CardDefaults.cardColors().containerColor, - ) - HorizontalDivider() - val items = remember { IntervalConfiguration.PAX_COUNTER.allowedIntervals } - DropDownPreference( - title = stringResource(Res.string.update_interval_seconds), - selectedItem = (formState.value.paxcounter_update_interval).toLong(), - enabled = state.connected, - items = items.map { it.value to it.toDisplayString() }, - onItemSelected = { - formState.value = - formState.value - .newBuilder() - .also { wb -> wb.paxcounter_update_interval = it.toInt() } - .build() - }, - ) - HorizontalDivider() - SignedIntegerEditTextPreference( - title = stringResource(Res.string.wifi_rssi_threshold_defaults_to_80), - value = formState.value.wifi_threshold, - enabled = state.connected, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - formState.value = formState.value.newBuilder().also { wb -> wb.wifi_threshold = it }.build() - }, - ) - HorizontalDivider() - SignedIntegerEditTextPreference( - title = stringResource(Res.string.ble_rssi_threshold_defaults_to_80), - value = formState.value.ble_threshold, - enabled = state.connected, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - formState.value = formState.value.newBuilder().also { wb -> wb.ble_threshold = it }.build() - }, - ) - } - } + item { PaxcounterSettings(formState = formState, enabled = state.connected) } + } +} + +@Composable +private fun PaxcounterSettings(formState: ConfigState, enabled: Boolean) { + val focusManager = LocalFocusManager.current + TitledCard(title = stringResource(Res.string.paxcounter_config)) { + SwitchPreference( + title = stringResource(Res.string.schema_paxcounter_enabled), + summary = stringResource(Res.string.schema_paxcounter_enabled_description), + checked = formState.value.enabled, + enabled = enabled, + onCheckedChange = { formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() + val items = remember { IntervalConfiguration.PAX_COUNTER.allowedIntervals } + DropDownPreference( + title = stringResource(Res.string.schema_paxcounter_paxcounter_update_interval), + summary = stringResource(Res.string.schema_paxcounter_paxcounter_update_interval_description), + selectedItem = (formState.value.paxcounter_update_interval).toLong(), + enabled = enabled, + items = items.map { it.value to it.toDisplayString() }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.paxcounter_update_interval = it.toInt() }.build() + }, + ) + HorizontalDivider() + SignedIntegerEditTextPreference( + title = + fieldTitle(Res.string.schema_paxcounter_wifi_threshold, ModuleConfig.PaxcounterConfig.wifi_threshold), + summary = stringResource(Res.string.schema_paxcounter_wifi_threshold_description), + value = formState.value.wifi_threshold, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.wifi_threshold = it }.build() + }, + ) + HorizontalDivider() + SignedIntegerEditTextPreference( + title = fieldTitle(Res.string.schema_paxcounter_ble_threshold, ModuleConfig.PaxcounterConfig.ble_threshold), + summary = stringResource(Res.string.schema_paxcounter_ble_threshold_description), + value = formState.value.ble_threshold, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.ble_threshold = it }.build() + }, + ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt index cbf408418c..1fd29f2a11 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt @@ -33,27 +33,31 @@ import org.meshtastic.core.model.Position import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.advanced_device_gps import org.meshtastic.core.resources.altitude -import org.meshtastic.core.resources.broadcast_interval -import org.meshtastic.core.resources.config_position_broadcast_secs_summary -import org.meshtastic.core.resources.config_position_broadcast_smart_minimum_distance_summary -import org.meshtastic.core.resources.config_position_broadcast_smart_minimum_interval_secs_summary -import org.meshtastic.core.resources.config_position_flags_summary -import org.meshtastic.core.resources.config_position_gps_update_interval_summary import org.meshtastic.core.resources.device_gps -import org.meshtastic.core.resources.fixed_position -import org.meshtastic.core.resources.gps_en_gpio -import org.meshtastic.core.resources.gps_mode -import org.meshtastic.core.resources.gps_receive_gpio -import org.meshtastic.core.resources.gps_transmit_gpio import org.meshtastic.core.resources.latitude import org.meshtastic.core.resources.longitude -import org.meshtastic.core.resources.minimum_distance -import org.meshtastic.core.resources.minimum_interval import org.meshtastic.core.resources.position -import org.meshtastic.core.resources.position_flags import org.meshtastic.core.resources.position_packet -import org.meshtastic.core.resources.smart_position -import org.meshtastic.core.resources.update_interval +import org.meshtastic.core.resources.schema_position_broadcast_smart_minimum_distance +import org.meshtastic.core.resources.schema_position_broadcast_smart_minimum_distance_description +import org.meshtastic.core.resources.schema_position_broadcast_smart_minimum_interval_secs +import org.meshtastic.core.resources.schema_position_broadcast_smart_minimum_interval_secs_description +import org.meshtastic.core.resources.schema_position_fixed_position +import org.meshtastic.core.resources.schema_position_fixed_position_description +import org.meshtastic.core.resources.schema_position_gps_en_gpio +import org.meshtastic.core.resources.schema_position_gps_en_gpio_description +import org.meshtastic.core.resources.schema_position_gps_mode +import org.meshtastic.core.resources.schema_position_gps_update_interval +import org.meshtastic.core.resources.schema_position_gps_update_interval_description +import org.meshtastic.core.resources.schema_position_position_broadcast_secs +import org.meshtastic.core.resources.schema_position_position_broadcast_secs_description +import org.meshtastic.core.resources.schema_position_position_broadcast_smart_enabled +import org.meshtastic.core.resources.schema_position_position_flags +import org.meshtastic.core.resources.schema_position_position_flags_description +import org.meshtastic.core.resources.schema_position_rx_gpio +import org.meshtastic.core.resources.schema_position_rx_gpio_description +import org.meshtastic.core.resources.schema_position_tx_gpio +import org.meshtastic.core.resources.schema_position_tx_gpio_description import org.meshtastic.core.ui.component.BitwisePreference import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference @@ -63,8 +67,10 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.feature.settings.util.FixedUpdateIntervals import org.meshtastic.feature.settings.util.IntervalConfiguration +import org.meshtastic.feature.settings.util.fieldTitle import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.Config +import org.meshtastic.proto.broadcast_smart_minimum_distance @Composable expect fun DeviceLocationButton( @@ -74,7 +80,7 @@ expect fun DeviceLocationButton( ) private val PositionStateSaver = - listSaver( + listSaver( save = { listOf( it.latitude, @@ -91,7 +97,7 @@ private val PositionStateSaver = Position( latitude = it[0] as Double, longitude = it[1] as Double, - altitude = it[2] as Int, + altitude = it[2] as Int?, time = it[3] as Int, satellitesInView = it[4] as Int, groundSpeed = it[5] as Int, @@ -110,7 +116,7 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un Position( latitude = node?.latitude ?: 0.0, longitude = node?.longitude ?: 0.0, - altitude = node?.position?.altitude ?: 0, + altitude = node?.position?.altitude, time = 1, // ignore time for fixed_position ) val positionConfig = state.radioConfig.position ?: Config.PositionConfig.Builder().build() @@ -179,8 +185,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un TitledCard(title = stringResource(Res.string.position_packet)) { val items = remember { IntervalConfiguration.POSITION_BROADCAST.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.broadcast_interval), - summary = stringResource(Res.string.config_position_broadcast_secs_summary), + title = stringResource(Res.string.schema_position_position_broadcast_secs), + summary = stringResource(Res.string.schema_position_position_broadcast_secs_description), enabled = state.connected, items = items.map { it to it.toDisplayString() }, selectedItem = @@ -196,7 +202,7 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.smart_position), + title = stringResource(Res.string.schema_position_position_broadcast_smart_enabled), checked = formState.value.position_broadcast_smart_enabled, enabled = state.connected, onCheckedChange = { @@ -209,9 +215,11 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un HorizontalDivider() val smartItems = remember { IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.minimum_interval), + title = stringResource(Res.string.schema_position_broadcast_smart_minimum_interval_secs), summary = - stringResource(Res.string.config_position_broadcast_smart_minimum_interval_secs_summary), + stringResource( + Res.string.schema_position_broadcast_smart_minimum_interval_secs_description, + ), enabled = state.connected, items = smartItems.map { it to it.toDisplayString() }, selectedItem = @@ -228,8 +236,13 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.minimum_distance), - summary = stringResource(Res.string.config_position_broadcast_smart_minimum_distance_summary), + title = + fieldTitle( + Res.string.schema_position_broadcast_smart_minimum_distance, + Config.PositionConfig.broadcast_smart_minimum_distance, + ), + summary = + stringResource(Res.string.schema_position_broadcast_smart_minimum_distance_description), value = formState.value.broadcast_smart_minimum_distance, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -247,7 +260,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un item { TitledCard(title = stringResource(Res.string.device_gps)) { SwitchPreference( - title = stringResource(Res.string.fixed_position), + title = stringResource(Res.string.schema_position_fixed_position), + summary = stringResource(Res.string.schema_position_fixed_position_description), checked = formState.value.fixed_position, enabled = state.connected, onCheckedChange = { @@ -283,7 +297,7 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un HorizontalDivider() EditTextPreference( title = stringResource(Res.string.altitude), - value = locationInput.altitude, + value = locationInput.altitude ?: 0, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { alt: Int -> locationInput = locationInput.copy(altitude = alt) }, @@ -297,9 +311,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un } else { HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.gps_mode), + title = stringResource(Res.string.schema_position_gps_mode), enabled = state.connected, - items = Config.PositionConfig.GpsMode.entries.map { it to it.name }, selectedItem = formState.value.gps_mode, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.gps_mode = it }.build() @@ -308,8 +321,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un HorizontalDivider() val items = remember { IntervalConfiguration.GPS_UPDATE.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.update_interval), - summary = stringResource(Res.string.config_position_gps_update_interval_summary), + title = stringResource(Res.string.schema_position_gps_update_interval), + summary = stringResource(Res.string.schema_position_gps_update_interval_description), enabled = state.connected, items = items.map { it to it.toDisplayString() }, selectedItem = @@ -327,10 +340,10 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un } } item { - TitledCard(title = stringResource(Res.string.position_flags)) { + TitledCard(title = stringResource(Res.string.schema_position_position_flags)) { BitwisePreference( - title = stringResource(Res.string.position_flags), - summary = stringResource(Res.string.config_position_flags_summary), + title = stringResource(Res.string.schema_position_position_flags), + summary = stringResource(Res.string.schema_position_position_flags_description), value = formState.value.position_flags, enabled = state.connected, items = @@ -347,7 +360,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un TitledCard(title = stringResource(Res.string.advanced_device_gps)) { val pins = remember { org.meshtastic.feature.settings.util.gpioPins } DropDownPreference( - title = stringResource(Res.string.gps_receive_gpio), + title = stringResource(Res.string.schema_position_rx_gpio), + summary = stringResource(Res.string.schema_position_rx_gpio_description), enabled = state.connected, items = pins, selectedItem = formState.value.rx_gpio, @@ -357,7 +371,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.gps_transmit_gpio), + title = stringResource(Res.string.schema_position_tx_gpio), + summary = stringResource(Res.string.schema_position_tx_gpio_description), enabled = state.connected, items = pins, selectedItem = formState.value.tx_gpio, @@ -367,7 +382,8 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.gps_en_gpio), + title = stringResource(Res.string.schema_position_gps_en_gpio), + summary = stringResource(Res.string.schema_position_gps_en_gpio_description), enabled = state.connected, items = pins, selectedItem = formState.value.gps_en_gpio, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt index 72393fa662..7d54508c74 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt @@ -27,16 +27,17 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.adc_multiplier_override -import org.meshtastic.core.resources.adc_multiplier_override_ratio import org.meshtastic.core.resources.battery_ina_2xx_i2c_address -import org.meshtastic.core.resources.config_power_is_power_saving_summary -import org.meshtastic.core.resources.enable_power_saving_mode import org.meshtastic.core.resources.minimum_wake_time_seconds import org.meshtastic.core.resources.power import org.meshtastic.core.resources.power_config -import org.meshtastic.core.resources.shutdown_on_power_loss +import org.meshtastic.core.resources.schema_power_adc_multiplier_override +import org.meshtastic.core.resources.schema_power_is_power_saving +import org.meshtastic.core.resources.schema_power_is_power_saving_description +import org.meshtastic.core.resources.schema_power_on_battery_shutdown_after_secs +import org.meshtastic.core.resources.schema_power_on_battery_shutdown_after_secs_description +import org.meshtastic.core.resources.schema_power_wait_bluetooth_secs import org.meshtastic.core.resources.super_deep_sleep_duration_seconds -import org.meshtastic.core.resources.wait_for_bluetooth_duration_seconds import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -68,8 +69,8 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.power_config)) { SwitchPreference( - title = stringResource(Res.string.enable_power_saving_mode), - summary = stringResource(Res.string.config_power_is_power_saving_summary), + title = stringResource(Res.string.schema_power_is_power_saving), + summary = stringResource(Res.string.schema_power_is_power_saving_description), checked = formState.value.is_power_saving, enabled = state.connected, onCheckedChange = { @@ -80,7 +81,8 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val items = remember { IntervalConfiguration.ALL.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.shutdown_on_power_loss), + title = stringResource(Res.string.schema_power_on_battery_shutdown_after_secs), + summary = stringResource(Res.string.schema_power_on_battery_shutdown_after_secs_description), selectedItem = formState.value.on_battery_shutdown_after_secs.toLong(), enabled = state.connected, items = items.map { it.value to it.toDisplayString() }, @@ -109,7 +111,7 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { if (formState.value.adc_multiplier_override > 0f) { HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.adc_multiplier_override_ratio), + title = stringResource(Res.string.schema_power_adc_multiplier_override), value = formState.value.adc_multiplier_override, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -122,7 +124,7 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val waitBluetoothItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.wait_for_bluetooth_duration_seconds), + title = stringResource(Res.string.schema_power_wait_bluetooth_secs), selectedItem = formState.value.wait_bluetooth_secs.toLong(), enabled = state.connected, items = waitBluetoothItems.map { it.value to it.toDisplayString() }, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt index bb3b521af7..8675a44ecb 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt @@ -26,9 +26,12 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.range_test import org.meshtastic.core.resources.range_test_config -import org.meshtastic.core.resources.range_test_enabled -import org.meshtastic.core.resources.save_csv_in_storage_esp32_only -import org.meshtastic.core.resources.sender_message_interval_seconds +import org.meshtastic.core.resources.schema_rangetest_enabled +import org.meshtastic.core.resources.schema_rangetest_enabled_description +import org.meshtastic.core.resources.schema_rangetest_save +import org.meshtastic.core.resources.schema_rangetest_save_description +import org.meshtastic.core.resources.schema_rangetest_sender +import org.meshtastic.core.resources.schema_rangetest_sender_description import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard @@ -65,7 +68,8 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.range_test_config)) { SwitchPreference( - title = stringResource(Res.string.range_test_enabled), + title = stringResource(Res.string.schema_rangetest_enabled), + summary = stringResource(Res.string.schema_rangetest_enabled_description), checked = formState.value.enabled, enabled = canConfigure || formState.value.enabled, onCheckedChange = { @@ -76,7 +80,8 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val rangeItems = remember { IntervalConfiguration.RANGE_TEST_SENDER.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.sender_message_interval_seconds), + title = stringResource(Res.string.schema_rangetest_sender), + summary = stringResource(Res.string.schema_rangetest_sender_description), selectedItem = (formState.value.sender).toLong(), enabled = canConfigure, items = rangeItems.map { it.value to it.toDisplayString() }, @@ -86,7 +91,8 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.save_csv_in_storage_esp32_only), + title = stringResource(Res.string.schema_rangetest_save), + summary = stringResource(Res.string.schema_rangetest_save_description), checked = formState.value.save, enabled = canConfigure, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt index f01d61af65..6c5861b7b8 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt @@ -35,30 +35,31 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Capabilities +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.model.util.encodeToString import org.meshtastic.core.model.util.platformRandomBytes import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.admin_key import org.meshtastic.core.resources.admin_keys import org.meshtastic.core.resources.administration -import org.meshtastic.core.resources.config_security_admin_key -import org.meshtastic.core.resources.config_security_debug_log_api_enabled -import org.meshtastic.core.resources.config_security_is_managed import org.meshtastic.core.resources.config_security_private_key import org.meshtastic.core.resources.config_security_private_key_remote -import org.meshtastic.core.resources.config_security_public_key -import org.meshtastic.core.resources.config_security_serial_enabled -import org.meshtastic.core.resources.debug_log_api_enabled import org.meshtastic.core.resources.direct_message_key import org.meshtastic.core.resources.logs -import org.meshtastic.core.resources.managed_mode -import org.meshtastic.core.resources.private_key -import org.meshtastic.core.resources.public_key import org.meshtastic.core.resources.redacted import org.meshtastic.core.resources.regenerate_keys_confirmation import org.meshtastic.core.resources.regenerate_private_key +import org.meshtastic.core.resources.schema_security_admin_key +import org.meshtastic.core.resources.schema_security_admin_key_description +import org.meshtastic.core.resources.schema_security_debug_log_api_enabled +import org.meshtastic.core.resources.schema_security_debug_log_api_enabled_description +import org.meshtastic.core.resources.schema_security_is_managed +import org.meshtastic.core.resources.schema_security_is_managed_description +import org.meshtastic.core.resources.schema_security_private_key +import org.meshtastic.core.resources.schema_security_public_key +import org.meshtastic.core.resources.schema_security_public_key_description +import org.meshtastic.core.resources.schema_security_serial_enabled +import org.meshtastic.core.resources.schema_security_serial_enabled_description import org.meshtastic.core.resources.security -import org.meshtastic.core.resources.serial_console import org.meshtastic.core.ui.component.CopyIconButton import org.meshtastic.core.ui.component.EditBase64Preference import org.meshtastic.core.ui.component.EditListPreference @@ -161,8 +162,8 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un item { TitledCard(title = stringResource(Res.string.admin_keys)) { EditListPreference( - title = stringResource(Res.string.admin_key), - summary = stringResource(Res.string.config_security_admin_key), + title = stringResource(Res.string.schema_security_admin_key), + summary = stringResource(Res.string.schema_security_admin_key_description), list = formState.value.admin_key, maxCount = 3, enabled = state.connected, @@ -176,8 +177,8 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un item { TitledCard(title = stringResource(Res.string.logs)) { SwitchPreference( - title = stringResource(Res.string.serial_console), - summary = stringResource(Res.string.config_security_serial_enabled), + title = stringResource(Res.string.schema_security_serial_enabled), + summary = stringResource(Res.string.schema_security_serial_enabled_description), checked = formState.value.serial_enabled, enabled = state.connected, onCheckedChange = { @@ -187,8 +188,8 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.debug_log_api_enabled), - summary = stringResource(Res.string.config_security_debug_log_api_enabled), + title = stringResource(Res.string.schema_security_debug_log_api_enabled), + summary = stringResource(Res.string.schema_security_debug_log_api_enabled_description), checked = formState.value.debug_log_api_enabled, enabled = state.connected, onCheckedChange = { @@ -202,8 +203,8 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un item { TitledCard(title = stringResource(Res.string.administration)) { SwitchPreference( - title = stringResource(Res.string.managed_mode), - summary = stringResource(Res.string.config_security_is_managed), + title = stringResource(Res.string.schema_security_is_managed), + summary = stringResource(Res.string.schema_security_is_managed_description), checked = formState.value.is_managed, enabled = state.connected && formState.value.admin_key.isNotEmpty(), onCheckedChange = { @@ -225,7 +226,7 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un passphrase = passphrase, boots = boots, hours = hours, - maxSessionSeconds = sessionMinutes * SECONDS_PER_MINUTE, + maxSessionSeconds = sessionMinutes * TimeConstants.SECONDS_PER_MINUTE, ) }, onDisable = { passphrase -> @@ -263,8 +264,8 @@ internal fun SecurityPublicKeyPreference( val publicKey = resolvedPublicKey(securityConfig, formState.value.private_key) EditBase64Preference( - title = stringResource(Res.string.public_key), - summary = stringResource(Res.string.config_security_public_key), + title = stringResource(Res.string.schema_security_public_key), + summary = stringResource(Res.string.schema_security_public_key_description), value = publicKey, enabled = enabled, readOnly = true, @@ -297,7 +298,7 @@ internal fun SecurityPrivateKeyPreference( val redacted = isPrivateKeyRedacted(securityConfig, isLocal) && privateKey.size != PRIVATE_KEY_SIZE EditBase64Preference( - title = stringResource(Res.string.private_key), + title = stringResource(Res.string.schema_security_private_key), summary = if (redacted) { stringResource(Res.string.config_security_private_key_remote) @@ -359,5 +360,3 @@ fun PrivateKeyRegenerateDialog( /** X25519 private key length in bytes. */ private const val PRIVATE_KEY_SIZE = 32 - -private const val SECONDS_PER_MINUTE = 60 diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt index 28d68aee5b..ff2f4c2dbb 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt @@ -25,16 +25,23 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.echo_enabled import org.meshtastic.core.resources.override_console_serial_port +import org.meshtastic.core.resources.schema_serial_baud +import org.meshtastic.core.resources.schema_serial_baud_description +import org.meshtastic.core.resources.schema_serial_echo +import org.meshtastic.core.resources.schema_serial_echo_description +import org.meshtastic.core.resources.schema_serial_enabled +import org.meshtastic.core.resources.schema_serial_enabled_description +import org.meshtastic.core.resources.schema_serial_mode +import org.meshtastic.core.resources.schema_serial_mode_description +import org.meshtastic.core.resources.schema_serial_rxd +import org.meshtastic.core.resources.schema_serial_rxd_description +import org.meshtastic.core.resources.schema_serial_timeout +import org.meshtastic.core.resources.schema_serial_timeout_description +import org.meshtastic.core.resources.schema_serial_txd +import org.meshtastic.core.resources.schema_serial_txd_description import org.meshtastic.core.resources.serial -import org.meshtastic.core.resources.serial_baud_rate import org.meshtastic.core.resources.serial_config -import org.meshtastic.core.resources.serial_enabled -import org.meshtastic.core.resources.serial_mode -import org.meshtastic.core.resources.serial_rx_pin -import org.meshtastic.core.resources.serial_tx_pin -import org.meshtastic.core.resources.timeout import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference @@ -66,7 +73,8 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.serial_config)) { SwitchPreference( - title = stringResource(Res.string.serial_enabled), + title = stringResource(Res.string.schema_serial_enabled), + summary = stringResource(Res.string.schema_serial_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -76,7 +84,8 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.echo_enabled), + title = stringResource(Res.string.schema_serial_echo), + summary = stringResource(Res.string.schema_serial_echo_description), checked = formState.value.echo, enabled = state.connected, onCheckedChange = { @@ -86,7 +95,8 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.serial_rx_pin), + title = stringResource(Res.string.schema_serial_rxd), + summary = stringResource(Res.string.schema_serial_rxd_description), value = formState.value.rxd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -96,7 +106,8 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.serial_tx_pin), + title = stringResource(Res.string.schema_serial_txd), + summary = stringResource(Res.string.schema_serial_txd_description), value = formState.value.txd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -106,9 +117,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.serial_baud_rate), + title = stringResource(Res.string.schema_serial_baud), + summary = stringResource(Res.string.schema_serial_baud_description), enabled = state.connected, - items = ModuleConfig.SerialConfig.Serial_Baud.entries.map { it to it.name }, selectedItem = formState.value.baud, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.baud = it }.build() @@ -116,7 +127,8 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.timeout), + title = stringResource(Res.string.schema_serial_timeout), + summary = stringResource(Res.string.schema_serial_timeout_description), value = formState.value.timeout, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -126,9 +138,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.serial_mode), + title = stringResource(Res.string.schema_serial_mode), + summary = stringResource(Res.string.schema_serial_mode_description), enabled = state.connected, - items = ModuleConfig.SerialConfig.Serial_Mode.entries.map { it to it.name }, selectedItem = formState.value.mode, onItemSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.mode = it }.build() diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt index ccefd87941..644f7a8df6 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res @@ -47,16 +48,18 @@ fun ShutdownConfirmationDialog( onDismiss: () -> Unit, isShutdown: Boolean = true, icon: ImageVector? = null, + warning: StringResource? = null, onConfirm: () -> Unit, ) { val nodeLongName = node?.user?.long_name ?: "Unknown Node" val resolvedIcon = icon ?: MeshtasticIcons.Warning + val resolvedWarning = warning ?: Res.string.shutdown_warning.takeIf { isShutdown } MeshtasticDialog( onDismiss = onDismiss, icon = resolvedIcon, title = title, - text = { ShutdownDialogContent(nodeLongName = nodeLongName, isShutdown = isShutdown) }, + text = { ShutdownDialogContent(nodeLongName = nodeLongName, warning = resolvedWarning) }, confirmText = stringResource(Res.string.send), onConfirm = { onDismiss() @@ -67,7 +70,7 @@ fun ShutdownConfirmationDialog( } @Composable -private fun ShutdownDialogContent(nodeLongName: String, isShutdown: Boolean) { +private fun ShutdownDialogContent(nodeLongName: String, warning: StringResource?) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { Text( text = stringResource(Res.string.shutdown_node_name, nodeLongName), @@ -77,10 +80,10 @@ private fun ShutdownDialogContent(nodeLongName: String, isShutdown: Boolean) { textAlign = TextAlign.Center, ) - if (isShutdown) { + if (warning != null) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(Res.string.shutdown_warning), + text = stringResource(warning), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, modifier = Modifier.fillMaxWidth(), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt index 66aa49c096..50d38fc433 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt @@ -25,14 +25,17 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.heartbeat -import org.meshtastic.core.resources.history_return_max -import org.meshtastic.core.resources.history_return_window -import org.meshtastic.core.resources.number_of_records -import org.meshtastic.core.resources.server +import org.meshtastic.core.resources.schema_storeforward_enabled +import org.meshtastic.core.resources.schema_storeforward_enabled_description +import org.meshtastic.core.resources.schema_storeforward_heartbeat +import org.meshtastic.core.resources.schema_storeforward_heartbeat_description +import org.meshtastic.core.resources.schema_storeforward_history_return_max +import org.meshtastic.core.resources.schema_storeforward_history_return_window +import org.meshtastic.core.resources.schema_storeforward_is_server +import org.meshtastic.core.resources.schema_storeforward_is_server_description +import org.meshtastic.core.resources.schema_storeforward_records import org.meshtastic.core.resources.store_forward import org.meshtastic.core.resources.store_forward_config -import org.meshtastic.core.resources.store_forward_enabled import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard @@ -63,7 +66,8 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit item { TitledCard(title = stringResource(Res.string.store_forward_config)) { SwitchPreference( - title = stringResource(Res.string.store_forward_enabled), + title = stringResource(Res.string.schema_storeforward_enabled), + summary = stringResource(Res.string.schema_storeforward_enabled_description), checked = formState.value.enabled, enabled = state.connected, onCheckedChange = { @@ -73,7 +77,8 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.heartbeat), + title = stringResource(Res.string.schema_storeforward_heartbeat), + summary = stringResource(Res.string.schema_storeforward_heartbeat_description), checked = formState.value.heartbeat, enabled = state.connected, onCheckedChange = { @@ -83,7 +88,7 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.number_of_records), + title = stringResource(Res.string.schema_storeforward_records), value = formState.value.records, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -93,7 +98,7 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.history_return_max), + title = stringResource(Res.string.schema_storeforward_history_return_max), value = formState.value.history_return_max, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -103,7 +108,7 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() EditTextPreference( - title = stringResource(Res.string.history_return_window), + title = stringResource(Res.string.schema_storeforward_history_return_window), value = formState.value.history_return_window, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -114,7 +119,8 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.server), + title = stringResource(Res.string.schema_storeforward_is_server), + summary = stringResource(Res.string.schema_storeforward_is_server_description), checked = formState.value.is_server, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 7b806de83c..fb9f2101d5 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -55,16 +55,16 @@ import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.Channel import org.meshtastic.core.model.getColorFrom -import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.TakPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.back import org.meshtastic.core.resources.export_tak_data_package +import org.meshtastic.core.resources.schema_tak_role +import org.meshtastic.core.resources.schema_tak_team import org.meshtastic.core.resources.tak import org.meshtastic.core.resources.tak_config -import org.meshtastic.core.resources.tak_role import org.meshtastic.core.resources.tak_server import org.meshtastic.core.resources.tak_server_channel import org.meshtastic.core.resources.tak_server_channel_desc @@ -95,7 +95,6 @@ import org.meshtastic.core.resources.tak_server_test_results_v2 import org.meshtastic.core.resources.tak_server_test_run import org.meshtastic.core.resources.tak_server_test_running import org.meshtastic.core.resources.tak_server_v1_fallback_notice -import org.meshtastic.core.resources.tak_team import org.meshtastic.core.takserver.TAKDataPackageGenerator import org.meshtastic.core.takserver.TAKMeshIntegration import org.meshtastic.core.takserver.TAKServerManager @@ -171,19 +170,17 @@ internal fun TakConfigCard( ) { TitledCard(title = stringResource(Res.string.tak_config)) { DropDownPreference( - title = stringResource(Res.string.tak_team), + title = stringResource(Res.string.schema_tak_team), enabled = enabled, selectedItem = team, - itemLabel = { stringResource(getStringResFrom(it)) }, itemColor = { Color(getColorFrom(it)) }, onItemSelected = onTeamSelected, ) HorizontalDivider() DropDownPreference( - title = stringResource(Res.string.tak_role), + title = stringResource(Res.string.schema_tak_role), enabled = enabled, selectedItem = role, - itemLabel = { stringResource(getStringResFrom(it)) }, onItemSelected = onRoleSelected, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt index 13b85be7d7..768f5d3d55 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt @@ -25,18 +25,28 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Capabilities import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.air_quality_metrics_module_enabled -import org.meshtastic.core.resources.air_quality_metrics_update_interval_seconds -import org.meshtastic.core.resources.device_metrics_update_interval_seconds -import org.meshtastic.core.resources.device_telemetry_enabled -import org.meshtastic.core.resources.device_telemetry_enabled_summary -import org.meshtastic.core.resources.environment_metrics_module_enabled -import org.meshtastic.core.resources.environment_metrics_on_screen_enabled -import org.meshtastic.core.resources.environment_metrics_update_interval_seconds -import org.meshtastic.core.resources.environment_metrics_use_fahrenheit -import org.meshtastic.core.resources.power_metrics_module_enabled -import org.meshtastic.core.resources.power_metrics_on_screen_enabled -import org.meshtastic.core.resources.power_metrics_update_interval_seconds +import org.meshtastic.core.resources.schema_telemetry_air_quality_enabled +import org.meshtastic.core.resources.schema_telemetry_air_quality_enabled_description +import org.meshtastic.core.resources.schema_telemetry_air_quality_interval +import org.meshtastic.core.resources.schema_telemetry_air_quality_interval_description +import org.meshtastic.core.resources.schema_telemetry_device_telemetry_enabled +import org.meshtastic.core.resources.schema_telemetry_device_telemetry_enabled_description +import org.meshtastic.core.resources.schema_telemetry_device_update_interval +import org.meshtastic.core.resources.schema_telemetry_device_update_interval_description +import org.meshtastic.core.resources.schema_telemetry_environment_display_fahrenheit +import org.meshtastic.core.resources.schema_telemetry_environment_display_fahrenheit_description +import org.meshtastic.core.resources.schema_telemetry_environment_measurement_enabled +import org.meshtastic.core.resources.schema_telemetry_environment_measurement_enabled_description +import org.meshtastic.core.resources.schema_telemetry_environment_screen_enabled +import org.meshtastic.core.resources.schema_telemetry_environment_screen_enabled_description +import org.meshtastic.core.resources.schema_telemetry_environment_update_interval +import org.meshtastic.core.resources.schema_telemetry_environment_update_interval_description +import org.meshtastic.core.resources.schema_telemetry_power_measurement_enabled +import org.meshtastic.core.resources.schema_telemetry_power_measurement_enabled_description +import org.meshtastic.core.resources.schema_telemetry_power_screen_enabled +import org.meshtastic.core.resources.schema_telemetry_power_screen_enabled_description +import org.meshtastic.core.resources.schema_telemetry_power_update_interval +import org.meshtastic.core.resources.schema_telemetry_power_update_interval_description import org.meshtastic.core.resources.telemetry import org.meshtastic.core.resources.telemetry_config import org.meshtastic.core.ui.component.DropDownPreference @@ -47,6 +57,7 @@ import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.feature.settings.util.IntervalConfiguration import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.device_telemetry_enabled @Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { @@ -72,10 +83,10 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) { item { TitledCard(title = stringResource(Res.string.telemetry_config)) { - if (capabilities.canToggleTelemetryEnabled) { + if (capabilities.offers(ModuleConfig.TelemetryConfig.device_telemetry_enabled)) { SwitchPreference( - title = stringResource(Res.string.device_telemetry_enabled), - summary = stringResource(Res.string.device_telemetry_enabled_summary), + title = stringResource(Res.string.schema_telemetry_device_telemetry_enabled), + summary = stringResource(Res.string.schema_telemetry_device_telemetry_enabled_description), checked = formState.value.device_telemetry_enabled, enabled = state.connected, onCheckedChange = { @@ -88,7 +99,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } val items = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.device_metrics_update_interval_seconds), + title = stringResource(Res.string.schema_telemetry_device_update_interval), + summary = stringResource(Res.string.schema_telemetry_device_update_interval_description), selectedItem = formState.value.device_update_interval.toLong(), enabled = state.connected, items = items.map { it.value to it.toDisplayString() }, @@ -99,7 +111,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.environment_metrics_module_enabled), + title = stringResource(Res.string.schema_telemetry_environment_measurement_enabled), + summary = stringResource(Res.string.schema_telemetry_environment_measurement_enabled_description), checked = formState.value.environment_measurement_enabled, enabled = state.connected, onCheckedChange = { @@ -111,7 +124,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val envItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.environment_metrics_update_interval_seconds), + title = stringResource(Res.string.schema_telemetry_environment_update_interval), + summary = stringResource(Res.string.schema_telemetry_environment_update_interval_description), selectedItem = formState.value.environment_update_interval.toLong(), enabled = state.connected, items = envItems.map { it.value to it.toDisplayString() }, @@ -125,7 +139,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.environment_metrics_on_screen_enabled), + title = stringResource(Res.string.schema_telemetry_environment_screen_enabled), + summary = stringResource(Res.string.schema_telemetry_environment_screen_enabled_description), checked = formState.value.environment_screen_enabled, enabled = state.connected, onCheckedChange = { @@ -136,7 +151,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.environment_metrics_use_fahrenheit), + title = stringResource(Res.string.schema_telemetry_environment_display_fahrenheit), + summary = stringResource(Res.string.schema_telemetry_environment_display_fahrenheit_description), checked = formState.value.environment_display_fahrenheit, enabled = state.connected, onCheckedChange = { @@ -147,7 +163,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.air_quality_metrics_module_enabled), + title = stringResource(Res.string.schema_telemetry_air_quality_enabled), + summary = stringResource(Res.string.schema_telemetry_air_quality_enabled_description), checked = formState.value.air_quality_enabled, enabled = state.connected, onCheckedChange = { @@ -159,7 +176,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val airItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.air_quality_metrics_update_interval_seconds), + title = stringResource(Res.string.schema_telemetry_air_quality_interval), + summary = stringResource(Res.string.schema_telemetry_air_quality_interval_description), selectedItem = formState.value.air_quality_interval.toLong(), enabled = state.connected, items = airItems.map { it.value to it.toDisplayString() }, @@ -170,7 +188,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.power_metrics_module_enabled), + title = stringResource(Res.string.schema_telemetry_power_measurement_enabled), + summary = stringResource(Res.string.schema_telemetry_power_measurement_enabled_description), checked = formState.value.power_measurement_enabled, enabled = state.connected, onCheckedChange = { @@ -182,7 +201,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { HorizontalDivider() val powerItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals } DropDownPreference( - title = stringResource(Res.string.power_metrics_update_interval_seconds), + title = stringResource(Res.string.schema_telemetry_power_update_interval), + summary = stringResource(Res.string.schema_telemetry_power_update_interval_description), selectedItem = formState.value.power_update_interval.toLong(), enabled = state.connected, items = powerItems.map { it.value to it.toDisplayString() }, @@ -193,7 +213,8 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { ) HorizontalDivider() SwitchPreference( - title = stringResource(Res.string.power_metrics_on_screen_enabled), + title = stringResource(Res.string.schema_telemetry_power_screen_enabled), + summary = stringResource(Res.string.schema_telemetry_power_screen_enabled_description), checked = formState.value.power_screen_enabled, enabled = state.connected, onCheckedChange = { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt index 6ea2f639c1..768c55e9e3 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt @@ -42,6 +42,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.HamName +import org.meshtastic.core.model.excludes import org.meshtastic.core.model.isUnmessageableRole import org.meshtastic.core.model.utf8Size import org.meshtastic.core.resources.Res @@ -66,6 +67,7 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.proto.ExcludedModules import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.User @@ -92,6 +94,8 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, focusS val formState = rememberConfigState(initialValue = userConfig) val firmwareVersion = state.metadata?.firmware_version val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } + val offersStatusMessage = + capabilities.supportsStatusMessage && !state.metadata.excludes(ExcludedModules.STATUSMESSAGE_CONFIG) // The status message is a ModuleConfig field, but it is part of the node's identity rather than a module of its // own, so it is edited beside the names instead of in a screen of its own. Editing it here also keeps it reachable @@ -105,7 +109,7 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, focusS val statusFocus = rememberStatusMessageFocus( requested = focusStatusMessage, - supported = capabilities.supportsStatusMessage, + supported = offersStatusMessage, connected = state.connected, ) // The field is absent without the capability, so the input can only differ from the saved value when shown. @@ -145,7 +149,7 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, focusS isLongNameError = !validLongName, isShortNameError = !validShortName, ) - if (capabilities.supportsStatusMessage) { + if (offersStatusMessage) { HorizontalDivider() StatusMessageField( value = statusMessageInput, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchBar.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchBar.kt new file mode 100644 index 0000000000..20c73806c3 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchBar.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.navigation.Route +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.settings_search_no_results +import org.meshtastic.core.resources.settings_search_placeholder +import org.meshtastic.core.ui.component.MeshtasticSearchBar + +/** Tag for the collapsed settings search field, so a test can target it rather than the expanded overlay's copy. */ +const val SETTINGS_SEARCH_BAR_INPUT_FIELD_TAG = "SettingsSearchBarInputField" + +/** + * The search field at the top of Settings. Typing expands the bar over the screen and lists matching settings; choosing + * one navigates to the screen that holds it. + * + * Results route to the destination, not to the individual control: android lays its settings screens out by hand, so + * there is no anchor to scroll to. Meshtastic-Apple lands on the control because its forms are generated. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsSearchBar( + viewModel: SettingsSearchViewModel, + onNavigate: (Route) -> Unit, + modifier: Modifier = Modifier, + includeAppLocal: Boolean = true, +) { + val query by viewModel.query.collectAsStateWithLifecycle() + val results by viewModel.results.collectAsStateWithLifecycle() + + LaunchedEffect(includeAppLocal) { viewModel.setIncludeAppLocal(includeAppLocal) } + + MeshtasticSearchBar( + query = query, + onQueryChange = viewModel::setQuery, + placeholder = stringResource(Res.string.settings_search_placeholder), + modifier = modifier, + inputFieldTag = SETTINGS_SEARCH_BAR_INPUT_FIELD_TAG, + expandedContent = { SettingsSearchResults(results = results, query = query, onSelect = onNavigate) }, + ) +} + +/** The results shown under the expanded field: the matches, or a line saying there were none. */ +@Composable +internal fun SettingsSearchResults( + results: List, + query: String, + onSelect: (Route) -> Unit, + modifier: Modifier = Modifier, +) { + if (query.isNotBlank() && results.isEmpty()) { + Column( + modifier = modifier.fillMaxSize().padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(Res.string.settings_search_no_results), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + // Keyed on the resource name, not the text: two entries can share a title and a destination (six controls are + // called "Enabled"), and a duplicate key crashes the list. + LazyColumn(modifier = modifier.fillMaxSize()) { + items(results, key = { it.id }) { entry -> + SettingsSearchResult(entry = entry, onClick = { onSelect(entry.route) }) + HorizontalDivider() + } + } + } +} + +@Composable +private fun SettingsSearchResult(entry: ResolvedSettingsEntry, onClick: () -> Unit) { + ListItem( + headlineContent = { Text(entry.title) }, + supportingContent = + entry.description?.let { description -> + { Text(text = description, maxLines = 2, overflow = TextOverflow.Ellipsis) } + }, + // The destination's name, so "Enabled" says which Enabled it is. + overlineContent = { Text(entry.screenTitle) }, + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + ) +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalog.kt new file mode 100644 index 0000000000..71d409408b --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalog.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.model.schemaEnumValuePrefixes +import org.meshtastic.core.navigation.Route +import org.meshtastic.core.navigation.SettingsRoute +import org.meshtastic.core.navigation.WifiProvisionRoute +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.about +import org.meshtastic.core.resources.acknowledgements +import org.meshtastic.core.resources.allStringResources +import org.meshtastic.core.resources.app_settings +import org.meshtastic.core.resources.debug_panel +import org.meshtastic.core.resources.device_links +import org.meshtastic.core.resources.filter_settings +import org.meshtastic.core.resources.node_layout_section_title +import org.meshtastic.core.resources.wifi_devices +import org.meshtastic.feature.settings.navigation.ConfigRoute +import org.meshtastic.feature.settings.navigation.ModuleRoute + +/** + * One thing the user can search for: a settings screen, or a single control on one. + * + * @param title the row's own name. For a proto-backed control this is the schema's label. + * @param description the schema's one-sentence explanation, where it has one. Matched as well as shown. + * @param route where tapping the result goes. + * @param screenTitle the destination's name, shown beside the result so "Enabled" says which Enabled it is. + */ +data class SettingsSearchEntry( + val title: StringResource, + val description: StringResource?, + val route: Route, + val screenTitle: StringResource, + /** True for settings that belong to this phone rather than to a radio, which a remote session must not offer. */ + val isAppLocal: Boolean = false, +) { + /** The title's resource name plus the destination. Unique across the catalog, which the results list keys on. */ + val id: String + get() = "${title.key}@$route" +} + +/** + * The searchable surface of Settings. + * + * Proto-backed entries are not listed here. They are read at runtime out of the generated `schema_strings.xml`, keyed + * by the prefix the schema path derives (`Config.LoRaConfig.hop_limit` is `schema_lora_hop_limit`), so a field the + * schema gains becomes searchable with no edit here. What is declared is only the part the schema cannot know: which + * settings destination owns a message. `SettingsSearchCatalogTest` fails when a prefix stops matching the registry, so + * the mapping cannot drift silently. + * + * App-level settings have no schema behind them and are listed by hand. + */ +object SettingsSearchCatalog { + + /** Suffix the generator gives a field's explanation; the base key is its title. */ + private const val DESCRIPTION_SUFFIX = "_description" + + private const val PREFIX = "schema_" + + /** + * Schema message prefixes owned by each configuration destination. + * + * A screen may own more than one message: the MQTT screen edits `MQTTConfig` and the `MapReportSettings` nested in + * it, and the TAK screen edits `TAKConfig` plus the two top-level enums its fields take. + */ + private val schemaPrefixes: Map> = + mapOf( + ConfigRoute.DEVICE.route to listOf("device"), + ConfigRoute.POSITION.route to listOf("position"), + ConfigRoute.POWER.route to listOf("power"), + ConfigRoute.NETWORK.route to listOf("network"), + ConfigRoute.DISPLAY.route to listOf("display"), + ConfigRoute.LORA.route to listOf("lora"), + ConfigRoute.BLUETOOTH.route to listOf("bluetooth"), + ConfigRoute.SECURITY.route to listOf("security"), + ModuleRoute.MQTT.route to listOf("mqtt", "mapreportsettings"), + ModuleRoute.SERIAL.route to listOf("serial"), + ModuleRoute.EXT_NOTIFICATION.route to listOf("externalnotification"), + ModuleRoute.STORE_FORWARD.route to listOf("storeforward"), + ModuleRoute.RANGE_TEST.route to listOf("rangetest"), + ModuleRoute.TELEMETRY.route to listOf("telemetry"), + ModuleRoute.CANNED_MESSAGE.route to listOf("cannedmessage"), + ModuleRoute.AUDIO.route to listOf("audio"), + ModuleRoute.REMOTE_HARDWARE.route to listOf("remotehardware"), + ModuleRoute.NEIGHBOR_INFO.route to listOf("neighborinfo"), + ModuleRoute.AMBIENT_LIGHTING.route to listOf("ambientlighting"), + ModuleRoute.DETECTION_SENSOR.route to listOf("detectionsensor"), + ModuleRoute.PAXCOUNTER.route to listOf("paxcounter"), + ModuleRoute.TAK.route to listOf("tak", "team", "memberrole"), + ModuleRoute.MESH_BEACON.route to listOf("meshbeacon"), + ) + + /** + * Message prefixes deliberately left out of the index, with the reason. + * + * `SettingsSearchCatalogTest` fails when a prefix in the generated file is neither claimed above nor excused here, + * so a message the schema starts labelling cannot quietly go missing from search. + */ + private val notSearchable: Map = + mapOf("trafficmanagement" to "no settings screen offers these fields yet") + + /** + * Keys that name an enum *value* rather than a field, so search does not offer "Long Fast" as though it were a + * setting. Meshtastic-Apple's settings search excludes them the same way (spec 019, FR-004). + * + * Generated alongside the enum labels, because nothing in a key's spelling separates `schema_bluetooth_fixed_pin` + * (a field) from `schema_bluetooth_pairingmode_fixed_pin` (a value of one). An enum the schema starts or stops + * labelling moves this set on the next sync rather than leaving search filtering on a stale copy. + */ + private val enumValuePrefixes: Set + get() = schemaEnumValuePrefixes + + /** The settings destinations themselves, so a query for a screen's own name finds it. */ + private fun screenEntries(): List = ( + ConfigRoute.entries.map { + it.title to it.route + } + ModuleRoute.entries.map { it.title to it.route } + ) + .map { (title, route) -> + SettingsSearchEntry(title = title, description = null, route = route, screenTitle = title) + } + + /** App-level settings, which have no schema behind them and so are listed by hand. */ + private val appSettings: List> = + listOf( + Res.string.node_layout_section_title to SettingsRoute.NodeList, + Res.string.wifi_devices to WifiProvisionRoute.WifiProvision(), + Res.string.filter_settings to SettingsRoute.FilterSettings, + Res.string.device_links to SettingsRoute.DeviceLinks, + Res.string.debug_panel to SettingsRoute.DebugPanel, + Res.string.about to SettingsRoute.About, + Res.string.acknowledgements to SettingsRoute.Acknowledgements, + ) + + private fun appEntries(): List = appSettings.map { (title, route) -> + SettingsSearchEntry( + title = title, + description = null, + route = route, + screenTitle = Res.string.app_settings, + isAppLocal = true, + ) + } + + /** Every proto-backed control the schema labels, attributed to the screen that owns its message. */ + private fun schemaEntries(): List { + val all = Res.allStringResources + val screenTitleByRoute = + ( + ConfigRoute.entries.associate { it.route to it.title } + + ModuleRoute.entries.associate { it.route to it.title } + ) + + return schemaPrefixes.flatMap { (route, messages) -> + val screenTitle = screenTitleByRoute[route] ?: return@flatMap emptyList() + messages.flatMap { message -> + val messagePrefix = PREFIX + message + "_" + all.keys + .asSequence() + .filter { it.startsWith(messagePrefix) } + .filterNot { it.endsWith(DESCRIPTION_SUFFIX) } + .filterNot { key -> enumValuePrefixes.any { key.startsWith(it) } } + .sorted() + .map { key -> + SettingsSearchEntry( + title = all.getValue(key), + description = all[key + DESCRIPTION_SUFFIX], + route = route, + screenTitle = screenTitle, + ) + } + .toList() + } + } + } + + /** Everything searchable, screens before the controls on them. */ + fun entries(): List = screenEntries() + appEntries() + schemaEntries() + + /** Exposed for the test that pins the mapping against the generated file. */ + internal fun declaredMessagePrefixes(): Set = schemaPrefixes.values.flatten().toSet() + + internal fun excusedMessagePrefixes(): Set = notSearchable.keys + + internal fun declaredEnumValuePrefixes(): Set = enumValuePrefixes +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcher.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcher.kt new file mode 100644 index 0000000000..7252748564 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcher.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import org.meshtastic.core.navigation.Route + +/** A catalog entry with its text resolved in the user's language, which is what the matcher reads. */ +data class ResolvedSettingsEntry( + /** [SettingsSearchEntry.id] of the entry this was resolved from. */ + val id: String, + val title: String, + val description: String?, + val screenTitle: String, + val route: Route, + /** True for settings that belong to this phone rather than to a radio. See [SettingsSearchEntry.isAppLocal]. */ + val isAppLocal: Boolean = false, +) + +/** + * Ranks settings results by where the query landed, following the weights Meshtastic-Apple's settings search shipped + * (spec 019, FR-011): a hit on the name outranks one in the explanation, and an exact name outranks a prefix. + * + * Case folding is Unicode-aware, so "lora" finds "LoRa" and "prasa" does not have to be typed as "Přáša" in the wrong + * case. Diacritics are not folded: "prasa" will not find "Přáša". That matches node search, which has the same limit. + */ +object SettingsSearchMatcher { + + private const val EXACT_TITLE = 100 + private const val TITLE_PREFIX = 75 + private const val TITLE_CONTAINS = 50 + private const val DESCRIPTION_CONTAINS = 25 + private const val SCREEN_CONTAINS = 10 + + fun rank(entries: List, query: String): List { + val needle = query.trim().lowercase() + if (needle.isEmpty()) return emptyList() + + return entries + .mapNotNull { entry -> score(entry, needle).takeIf { it > 0 }?.let { entry to it } } + // Descending score, then by name, so a term matching many entries orders the same way every time. + .sortedWith(compareByDescending> { it.second }.thenBy { it.first.title }) + .map { it.first } + } + + private fun score(entry: ResolvedSettingsEntry, needle: String): Int { + val title = entry.title.lowercase() + var score = + when { + title == needle -> EXACT_TITLE + title.startsWith(needle) -> TITLE_PREFIX + title.contains(needle) -> TITLE_CONTAINS + else -> 0 + } + // A hit in the explanation and a hit in the name add, so an entry matching both outranks one matching either. + if (entry.description?.lowercase()?.contains(needle) == true) score += DESCRIPTION_CONTAINS + if (entry.screenTitle.lowercase().contains(needle)) score += SCREEN_CONTAINS + return score + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchPreviews.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchPreviews.kt new file mode 100644 index 0000000000..df56f19e1a --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchPreviews.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.navigation.SettingsRoute +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.app_settings +import org.meshtastic.core.resources.device +import org.meshtastic.core.resources.lora +import org.meshtastic.core.resources.node_layout_section_title +import org.meshtastic.core.resources.schema_device_rebroadcast_mode +import org.meshtastic.core.resources.schema_lora_hop_limit +import org.meshtastic.core.resources.schema_lora_hop_limit_description +import org.meshtastic.core.ui.theme.AppTheme + +/** + * What a query for "hop" matches, resolved from the same resources the index reads, so the preview shows the wording + * the schema actually ships and follows it into every other language. + */ +@Composable +private fun hopResults(): List = listOf( + ResolvedSettingsEntry( + id = "hop_limit", + title = stringResource(Res.string.schema_lora_hop_limit), + description = stringResource(Res.string.schema_lora_hop_limit_description), + screenTitle = stringResource(Res.string.lora), + route = SettingsRoute.LoRa, + ), + // A field the schema labels but does not explain, which is the commonest shape in the index. + ResolvedSettingsEntry( + id = "rebroadcast_mode", + title = stringResource(Res.string.schema_device_rebroadcast_mode), + description = null, + screenTitle = stringResource(Res.string.device), + route = SettingsRoute.Device, + ), + // An app-level entry, with no schema behind it at all. + ResolvedSettingsEntry( + id = "node_layout", + title = stringResource(Res.string.node_layout_section_title), + description = null, + screenTitle = stringResource(Res.string.app_settings), + route = SettingsRoute.NodeList, + isAppLocal = true, + ), +) + +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun SettingsSearchResultsPreview() { + AppTheme { Surface { SettingsSearchResults(results = hopResults(), query = "hop", onSelect = {}) } } +} + +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun SettingsSearchNoResultsPreview() { + AppTheme { Surface { SettingsSearchResults(results = emptyList(), query = "xyzzy", onSelect = {}) } } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchViewModel.kt new file mode 100644 index 0000000000..6532cd1921 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/search/SettingsSearchViewModel.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.resources.getStringSuspend + +/** + * Backs the search field on the Settings screen. + * + * The catalog's text is resolved once, not per keystroke: reading a string resource suspends, and there are several + * hundred of them. Matching then runs over the resolved copy. + */ +@KoinViewModel +class SettingsSearchViewModel : ViewModel() { + + private val _query = MutableStateFlow("") + val query: StateFlow = _query.asStateFlow() + + private val resolved = MutableStateFlow>(emptyList()) + + /** + * Whether this phone's own settings are offered. False while administering another node remotely: both settings + * screens hide their app-settings section then, and search must not be a way back in to what they hide. + */ + private val includeAppLocal = MutableStateFlow(true) + + val results: StateFlow> = + combine(resolved, _query, includeAppLocal) { entries, query, includeLocal -> + SettingsSearchMatcher.rank(entries.filter { includeLocal || !it.isAppLocal }, query) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS), emptyList()) + + init { + viewModelScope.launch { + resolved.value = + SettingsSearchCatalog.entries().map { entry -> + ResolvedSettingsEntry( + id = entry.id, + title = getStringSuspend(entry.title), + description = entry.description?.let { getStringSuspend(it) }, + screenTitle = getStringSuspend(entry.screenTitle), + route = entry.route, + isAppLocal = entry.isAppLocal, + ) + } + } + } + + fun setQuery(query: String) { + _query.value = query + } + + fun setIncludeAppLocal(include: Boolean) { + includeAppLocal.value = include + } + + private companion object { + const val STOP_TIMEOUT_MILLIS = 5_000L + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt index 14412218d6..ef5c81ded5 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt @@ -17,12 +17,17 @@ package org.meshtastic.feature.settings.tak import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import org.meshtastic.core.ui.util.rememberFileExporter /** - * Platform-specific composable that returns a launcher for exporting a TAK data package zip. + * Returns a launcher for exporting a TAK data package zip. * * @param dataPackageProvider suspend function producing the zip [ByteArray] * @return a lambda accepting the suggested file name to trigger the export */ @Composable -expect fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit +fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit { + val export = rememberFileExporter(content = dataPackageProvider) + return remember(export) { { fileName -> export(fileName, "application/zip") } } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnits.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnits.kt new file mode 100644 index 0000000000..cf5175030d --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnits.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.util + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.label_with_unit +import org.meshtastic.core.resources.unit_dbm +import org.meshtastic.core.resources.unit_khz +import org.meshtastic.core.resources.unit_meters +import org.meshtastic.proto.FieldMetadata + +/** + * The translated name of the unit a proto field declares, or null where the app has no name for that symbol. The + * schema's symbol is never rendered directly: a symbol with no entry here shows no unit rather than raw schema text. + */ +val FieldMetadata.unitLabelRes: StringResource? + get() = + when (unit) { + "m" -> Res.string.unit_meters + "dBm" -> Res.string.unit_dbm + "kHz" -> Res.string.unit_khz + else -> null + } + +/** + * A control's title, with the schema's unit appended when the control shows a bare number. An interval picker names its + * own unit in every item ("30 seconds", "2 hours"), so those pass no metadata and keep the label alone. + */ +@Composable +fun fieldTitle(label: StringResource, metadata: FieldMetadata): String { + val unitRes = metadata.unitLabelRes ?: return stringResource(label) + return stringResource(Res.string.label_with_unit, stringResource(label), stringResource(unitRes)) +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurations.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurations.kt new file mode 100644 index 0000000000..2ec16c44f5 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurations.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.util + +import org.jetbrains.compose.resources.PluralStringResource +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.interval_unset +import org.meshtastic.core.resources.plurals_milliseconds +import org.meshtastic.core.resources.plurals_seconds +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * How long the external notification output stays driven, in **milliseconds**, which is the unit + * `ModuleConfig.ExternalNotificationConfig.output_ms` is read in. [FixedUpdateIntervals] cannot serve this field: its + * values are seconds, and writing one here sets a pulse a thousand times shorter than the label promises. + * + * The sub-second entries are the durations the firmware itself defaults to, so a radio that has never been configured + * from this app still matches a row instead of reading as unset. + */ +enum class FixedOutputDurations( + val value: Long, + val textRes: StringResource? = null, + val pluralRes: PluralStringResource? = null, + val quantity: Int? = null, +) { + UNSET(0L, textRes = Res.string.interval_unset), + ONE_HUNDRED_MILLISECONDS( + 100.milliseconds.inWholeMilliseconds, + pluralRes = Res.plurals.plurals_milliseconds, + quantity = 100, + ), + FIVE_HUNDRED_MILLISECONDS( + 500.milliseconds.inWholeMilliseconds, + pluralRes = Res.plurals.plurals_milliseconds, + quantity = 500, + ), + ONE_SECOND(1.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 1), + TWO_SECONDS(2.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 2), + THREE_SECONDS(3.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 3), + FOUR_SECONDS(4.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 4), + FIVE_SECONDS(5.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 5), + TEN_SECONDS(10.seconds.inWholeMilliseconds, pluralRes = Res.plurals.plurals_seconds, quantity = 10), + ; + + companion object { + /** The durations the picker offers, in order. */ + val allowed: List = entries + } +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt index 80e1c11216..f4177e309b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt @@ -103,7 +103,6 @@ enum class IntervalConfiguration { DETECTION_SENSOR_MINIMUM, DETECTION_SENSOR_STATE, NAG_TIMEOUT, - OUTPUT, PAX_COUNTER, POSITION, POSITION_BROADCAST, @@ -199,17 +198,6 @@ enum class IntervalConfiguration { FixedUpdateIntervals.SEVENTY_TWO_HOURS, ) - OUTPUT -> - listOf( - FixedUpdateIntervals.UNSET, - FixedUpdateIntervals.ONE_SECOND, - FixedUpdateIntervals.TWO_SECONDS, - FixedUpdateIntervals.THREE_SECONDS, - FixedUpdateIntervals.FOUR_SECONDS, - FixedUpdateIntervals.FIVE_SECONDS, - FixedUpdateIntervals.TEN_SECONDS, - ) - DETECTION_SENSOR_MINIMUM -> listOf( FixedUpdateIntervals.UNSET, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt index c75268adca..3ad0fbe9fa 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt @@ -28,3 +28,12 @@ fun FixedUpdateIntervals.toDisplayString(): String = if (pluralRes != null && qu } else { "" } + +@Composable +fun FixedOutputDurations.toDisplayString(): String = if (pluralRes != null && quantity != null) { + pluralStringResource(pluralRes, quantity, quantity) +} else if (textRes != null) { + stringResource(textRes) +} else { + "" +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 83c2c037a9..c3023d4228 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -54,6 +54,7 @@ import org.meshtastic.core.model.MeshLog import org.meshtastic.core.repository.FileService import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.testing.FakeAppPreferences +import org.meshtastic.core.testing.FakeApplicationCoroutineScope import org.meshtastic.core.testing.FakeDatabaseManager import org.meshtastic.core.testing.FakeMeshLogRepository import org.meshtastic.core.testing.FakeNodeRepository @@ -105,7 +106,12 @@ class SettingsViewModelTest { every { isOtaCapableUseCase() } returns flowOf(true) val uiPrefs = appPreferences.ui - val setMeshLogSettingsUseCase = SetMeshLogSettingsUseCase(meshLogRepository, appPreferences.meshLog) + val setMeshLogSettingsUseCase = + SetMeshLogSettingsUseCase( + meshLogRepository, + appPreferences.meshLog, + FakeApplicationCoroutineScope(testDispatcher), + ) val exportDataUseCase = ExportDataUseCase(nodeRepository, meshLogRepository) val exportNodeDatabaseUseCase = ExportNodeDatabaseUseCase(nodeRepository) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModelTest.kt new file mode 100644 index 0000000000..9e865792c8 --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsViewModelTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.appfunctions + +import org.meshtastic.core.repository.AppFunctionsPrefs +import org.meshtastic.core.repository.AppFunctionsSetting +import org.meshtastic.core.testing.FakeAppFunctionsPrefs +import kotlin.test.Test +import kotlin.test.assertEquals + +class AppFunctionsSettingsViewModelTest { + + @Test + fun `toggle hands every setting to the prefs store toggle`() { + val toggled = mutableListOf() + val prefs = + object : AppFunctionsPrefs by FakeAppFunctionsPrefs() { + override fun toggle(setting: AppFunctionsSetting) { + toggled += setting + } + } + val viewModel = AppFunctionsSettingsViewModel(prefs) + + AppFunctionsSetting.entries.forEach(viewModel::toggle) + + assertEquals>(AppFunctionsSetting.entries, toggled) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapScreenProvider.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTermsTest.kt similarity index 53% rename from core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapScreenProvider.kt rename to feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTermsTest.kt index 26eb02b7e8..97dc010a1d 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapScreenProvider.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTermsTest.kt @@ -14,18 +14,26 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.ui.util +package org.meshtastic.feature.settings.debugging -import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf -import org.meshtastic.core.ui.component.PlaceholderScreen +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue -/** - * Provides the platform-specific Traceroute Map Screen. On Desktop or JVM targets where native maps aren't available - * yet, it falls back to a [PlaceholderScreen]. - */ -@Suppress("Wrapping") -val LocalTracerouteMapScreenProvider = - compositionLocalOf<@Composable (destNum: Int, requestId: Int, logUuid: String?, onNavigateUp: () -> Unit) -> Unit> { - { _, _, _, _ -> PlaceholderScreen("Traceroute Map") } +class DebugSearchTermsTest { + + @Test + fun `blank terms from repeated spaces are dropped`() { + assertEquals(2, compileSearchTerms("alpha beta ").size) + assertTrue(compileSearchTerms("").isEmpty()) } + + @Test + fun `terms match literally and ignore case`() { + val (term) = compileSearchTerms("a.b") + + assertTrue(term.containsMatchIn("xA.By")) + assertFalse(term.containsMatchIn("axb")) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt index 4708f07b0d..0d2d485aa7 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt @@ -16,14 +16,17 @@ */ package org.meshtastic.feature.settings.debugging +import androidx.lifecycle.viewModelScope import dev.mokkery.MockMode import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent @@ -31,8 +34,10 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase import org.meshtastic.core.model.MeshLog import org.meshtastic.core.repository.MeshLogRetention +import org.meshtastic.core.testing.FakeApplicationCoroutineScope import org.meshtastic.core.testing.FakeMeshLogPrefs import org.meshtastic.core.testing.FakeMeshLogRepository import org.meshtastic.core.testing.FakeNodeRepository @@ -71,6 +76,12 @@ class DebugViewModelTest { meshLogRepository = meshLogRepository, nodeRepository = nodeRepository, meshLogPrefs = meshLogPrefs, + setMeshLogSettingsUseCase = + SetMeshLogSettingsUseCase( + meshLogRepository, + meshLogPrefs, + FakeApplicationCoroutineScope(testDispatcher), + ), alertManager = alertManager, dispatchers = dispatchers, ) @@ -123,6 +134,23 @@ class DebugViewModelTest { meshLogRepository.currentLogs.map { it.uuid } shouldBe listOf("ancient", "recent") } + @Test + fun `retention prune keeps running after the view model is cleared`() = runTest { + val now = nowMillis + meshLogRepository.setLogs( + listOf(MeshLog("recent", "TEXT", now, ""), MeshLog("stale", "TEXT", now - 30.days.inWholeMilliseconds, "")), + ) + val releasePrune = CompletableDeferred() + meshLogRepository.beforeDeleteLogsOlderThan = { releasePrune.await() } + + viewModel.setRetentionDays(14) + meshLogRepository.deleteLogsOlderThanCalls shouldBe 1 + viewModel.viewModelScope.cancel() + releasePrune.complete(Unit) + + meshLogRepository.currentLogs.map { it.uuid } shouldBe listOf("recent") + } + @Test fun `setLoggingEnabled false deletes all logs`() = runTest { meshLogRepository.insert(org.meshtastic.core.model.MeshLog("123", "type", 1L, "raw")) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt index 08e8a89892..96da2352cf 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt @@ -17,29 +17,25 @@ package org.meshtastic.feature.settings.filter import dev.mokkery.MockMode -import dev.mokkery.answering.returns -import dev.mokkery.every -import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify -import kotlinx.coroutines.flow.MutableStateFlow -import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter +import org.meshtastic.core.testing.FakeFilterPrefs import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals class FilterSettingsViewModelTest { - private val filterPrefs: FilterPrefs = mock(MockMode.autofill) + private val filterPrefs = FakeFilterPrefs() private val messageFilter: MessageFilter = mock(MockMode.autofill) private lateinit var viewModel: FilterSettingsViewModel @BeforeTest fun setUp() { - every { filterPrefs.filterEnabled } returns MutableStateFlow(true) - every { filterPrefs.filterWords } returns MutableStateFlow(setOf("apple", "banana")) + filterPrefs.setFilterEnabled(true) + filterPrefs.setFilterWords(setOf("apple", "banana")) viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilter = messageFilter) } @@ -47,25 +43,35 @@ class FilterSettingsViewModelTest { @Test fun setFilterEnabled_updates_prefs_and_state() { viewModel.setFilterEnabled(false) - verify { filterPrefs.setFilterEnabled(false) } + assertEquals(false, filterPrefs.filterEnabled.value) assertEquals(false, viewModel.filterEnabled.value) } + @Test + fun state_follows_prefs_that_load_after_creation() { + val coldPrefs = FakeFilterPrefs() + val coldViewModel = FilterSettingsViewModel(filterPrefs = coldPrefs, messageFilter = messageFilter) + + coldPrefs.setFilterEnabled(true) + coldPrefs.setFilterWords(setOf("cherry")) + + assertEquals(true, coldViewModel.filterEnabled.value) + assertEquals(setOf("cherry"), coldViewModel.filterWords.value) + } + @Test fun addFilterWord_updates_prefs_and_rebuilds_patterns() { viewModel.addFilterWord("cherry") - verify { filterPrefs.setFilterWords(any()) } verify { messageFilter.rebuildPatterns() } - assertEquals(listOf("apple", "banana", "cherry"), viewModel.filterWords.value) + assertEquals(setOf("apple", "banana", "cherry"), viewModel.filterWords.value) } @Test fun removeFilterWord_updates_prefs_and_rebuilds_patterns() { viewModel.removeFilterWord("apple") - verify { filterPrefs.setFilterWords(any()) } verify { messageFilter.rebuildPatterns() } - assertEquals(listOf("banana"), viewModel.filterWords.value) + assertEquals(setOf("banana"), viewModel.filterWords.value) } } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/lockdown/LockdownPassphraseValidationTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/lockdown/LockdownPassphraseValidationTest.kt new file mode 100644 index 0000000000..0eb9a6560e --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/lockdown/LockdownPassphraseValidationTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.lockdown + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class LockdownPassphraseValidationTest { + + @Test + fun `an empty passphrase is rejected`() { + assertFalse(isValidLockdownPassphrase("")) + } + + @Test + fun `the firmware limit is counted in bytes not characters`() { + assertTrue(isValidLockdownPassphrase("a".repeat(64))) + assertFalse(isValidLockdownPassphrase("a".repeat(65))) + // Two bytes each in UTF-8: 32 fit, 33 do not. + assertTrue(isValidLockdownPassphrase("é".repeat(32))) + assertFalse(isValidLockdownPassphrase("é".repeat(33))) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/navigation/ModuleRouteTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/navigation/ModuleRouteTest.kt new file mode 100644 index 0000000000..da67bd7c95 --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/navigation/ModuleRouteTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.navigation + +import org.meshtastic.proto.Config +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.ExcludedModules +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class ModuleRouteTest { + + private fun metadata(excluded: Int = 0, firmware: String = "2.8.0") = DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = firmware + wb.excluded_modules = excluded + } + .build() + + private fun routes(excluded: Int = 0, role: Config.DeviceConfig.Role = Config.DeviceConfig.Role.TAK) = + ModuleRoute.filterExcludedFrom(metadata(excluded), role) + + @Test + fun `every route carries its own excluded_modules bit`() { + val bits = ModuleRoute.entries.map { it.excludedAs } + + assertEquals(bits.size, bits.toSet().size) + assertFalse(ExcludedModules.EXCLUDED_NONE in bits) + } + + @Test + fun `a TAK role node on 2 8 firmware lists the TAK and Mesh Beacon modules`() { + val listed = routes() + + assertContains(listed, ModuleRoute.TAK) + assertContains(listed, ModuleRoute.MESH_BEACON) + } + + @Test + fun `the TAK bit hides the TAK module and nothing else`() { + val listed = routes(excluded = ExcludedModules.TAK_CONFIG.value) + + assertEquals(ModuleRoute.entries - ModuleRoute.TAK, listed) + } + + @Test + fun `the Mesh Beacon bit hides the Mesh Beacon module and nothing else`() { + val listed = routes(excluded = ExcludedModules.MESHBEACON_CONFIG.value) + + assertEquals(ModuleRoute.entries - ModuleRoute.MESH_BEACON, listed) + } + + @Test + fun `the status message and traffic management bits hide no module route`() { + val excluded = ExcludedModules.STATUSMESSAGE_CONFIG.value or ExcludedModules.TRAFFICMANAGEMENT_CONFIG.value + + assertEquals(ModuleRoute.entries.toList(), routes(excluded = excluded)) + } + + @Test + fun `an older bit still hides its module`() { + val listed = routes(excluded = ExcludedModules.MQTT_CONFIG.value or ExcludedModules.PAXCOUNTER_CONFIG.value) + + assertEquals(ModuleRoute.entries - ModuleRoute.MQTT - ModuleRoute.PAXCOUNTER, listed) + } + + @Test + fun `metadata not read yet excludes nothing and leaves only the version gates`() { + val listed = ModuleRoute.filterExcludedFrom(null, Config.DeviceConfig.Role.TAK) + + assertEquals(ModuleRoute.entries - ModuleRoute.TAK - ModuleRoute.MESH_BEACON, listed) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt index e2b564a301..8787b66194 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt @@ -19,6 +19,7 @@ package org.meshtastic.feature.settings.radio import dev.mokkery.MockMode import dev.mokkery.answering.returns import dev.mokkery.every +import dev.mokkery.matcher.any import dev.mokkery.mock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,6 +27,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain @@ -45,6 +47,7 @@ import org.meshtastic.core.domain.usecase.settings.ProcessRadioResponseUseCase import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.repository.AnalyticsPrefs +import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FileService import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.LocationRepository @@ -87,6 +90,7 @@ class ProfileRoundTripTest { private val importSecurityConfigUseCase: ImportSecurityConfigUseCase = mock(MockMode.autofill) private val securityKeyBackupStore: SecurityKeyBackupStore = mock(MockMode.autofill) private val snackbarManager: SnackbarManager = mock(MockMode.autofill) + private val deviceHardwareRepository: DeviceHardwareRepository = mock(MockMode.autofill) private val nodeRestartTracker = NodeRestartTracker(CoroutineScope(SupervisorJob())) private val installProfileUseCase: InstallProfileUseCase = mock(MockMode.autofill) private val radioConfigUseCase: RadioConfigUseCase = mock(MockMode.autofill) @@ -115,6 +119,7 @@ class ProfileRoundTripTest { every { serviceRepository.meshPacketFlow } returns MutableSharedFlow() every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) + every { deviceHardwareRepository.observeDeviceHardware(any(), any()) } returns flowOf(null) every { mqttManager.mqttConnectionState } returns MutableStateFlow(org.meshtastic.core.model.MqttConnectionState.Inactive) @@ -124,6 +129,7 @@ class ProfileRoundTripTest { radioConfigRepository = radioConfigRepository, serviceRepository = serviceRepository, nodeRepository = nodeRepository, + deviceHardwareRepository = deviceHardwareRepository, locationRepository = locationRepository, mapConsentPrefs = mapConsentPrefs, analyticsPrefs = analyticsPrefs, diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index 138cd86e8f..47f2fd10f9 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy @@ -55,11 +56,13 @@ import org.meshtastic.core.domain.usecase.settings.ProcessRadioResponseUseCase import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase import org.meshtastic.core.domain.usecase.settings.RadioResponseResult import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.MalformedMeshtasticUrlException import org.meshtastic.core.repository.AnalyticsPrefs +import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.FileService import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.LocationRepository @@ -87,7 +90,9 @@ import org.meshtastic.proto.Config import org.meshtastic.proto.Data import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.DeviceProfile +import org.meshtastic.proto.ExcludedModules import org.meshtastic.proto.HamParameters +import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LoRaPresetGroup import org.meshtastic.proto.LoRaRegionPresetMap import org.meshtastic.proto.LocalConfig @@ -149,7 +154,7 @@ class RadioConfigViewModelTest { Node( num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build(), - metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(), ) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel(destNum = 123) @@ -160,6 +165,26 @@ class RadioConfigViewModelTest { verifySuspend(exactly(0)) { radioConfigUseCase.getModuleConfig(any(), any(), any()) } } + @Test + fun `USER route skips the status message config when the firmware compiled the module out`() = runTest { + val metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.8.0" + wb.excluded_modules = ExcludedModules.STATUSMESSAGE_CONFIG.value + } + .build() + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build(), metadata = metadata) + nodeRepository.setNodes(listOf(node)) + viewModel = createViewModel(destNum = 123) + + viewModel.setResponseStateLoading(ConfigRoute.USER) + advanceUntilIdle() + + verifySuspend { radioConfigUseCase.getOwner(123, any()) } + verifySuspend(exactly(0)) { radioConfigUseCase.getModuleConfig(any(), any(), any()) } + } + @Test fun `MESH_BEACON route on a remote node reads the beacon module config plus LoRa config and channel 0`() = runTest { val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) @@ -196,7 +221,7 @@ class RadioConfigViewModelTest { Node( num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build(), - metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(), ) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) @@ -378,6 +403,7 @@ class RadioConfigViewModelTest { private val installProfileUseCase: InstallProfileUseCase = mock(MockMode.autofill) private val radioConfigUseCase: RadioConfigUseCase = mock(MockMode.autofill) private val adminActionsUseCase: AdminActionsUseCase = mock(MockMode.autofill) + private val deviceHardwareRepository: DeviceHardwareRepository = mock(MockMode.autofill) private val processRadioResponseUseCase: ProcessRadioResponseUseCase = mock(MockMode.autofill) private val locationService: LocationService = mock(MockMode.autofill) private val fileService: FileService = mock(MockMode.autofill) @@ -422,6 +448,7 @@ class RadioConfigViewModelTest { every { mqttManager.proxyActive } returns MutableStateFlow(false) every { uiPrefs.showQuickChat } returns MutableStateFlow(false) + every { deviceHardwareRepository.observeDeviceHardware(any(), any()) } returns flowOf(null) viewModel = createViewModel() } @@ -447,6 +474,7 @@ class RadioConfigViewModelTest { radioConfigRepository = radioConfigRepository, serviceRepository = serviceRepository, nodeRepository = nodeRepository, + deviceHardwareRepository = deviceHardwareRepository, locationRepository = locationRepository, mapConsentPrefs = mapConsentPrefs, analyticsPrefs = analyticsPrefs, @@ -621,23 +649,21 @@ class RadioConfigViewModelTest { } @Test - fun `toggleAnalyticsAllowed calls prefs`() { - every { analyticsPrefs.analyticsAllowed } returns MutableStateFlow(true) - every { analyticsPrefs.setAnalyticsAllowed(false) } returns Unit + fun `toggleAnalyticsAllowed delegates to the prefs toggle`() { + every { analyticsPrefs.toggleAnalyticsAllowed() } returns Unit viewModel.toggleAnalyticsAllowed() - verify { analyticsPrefs.setAnalyticsAllowed(false) } + verify { analyticsPrefs.toggleAnalyticsAllowed() } } @Test - fun `toggleHomoglyphCharactersEncodingEnabled calls prefs`() { - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns MutableStateFlow(true) - every { homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(false) } returns Unit + fun `toggleHomoglyphCharactersEncodingEnabled delegates to the prefs toggle`() { + every { homoglyphEncodingPrefs.toggleHomoglyphEncodingEnabled() } returns Unit viewModel.toggleHomoglyphCharactersEncodingEnabled() - verify { homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(false) } + verify { homoglyphEncodingPrefs.toggleHomoglyphEncodingEnabled() } } @Test @@ -1106,7 +1132,7 @@ class RadioConfigViewModelTest { } every { processRadioResponseUseCase(any(), 123, any()) } calls { - val pendingRequestIds = it.args[2] as Set + val pendingRequestIds = it.arg>(2) if (42 in pendingRequestIds) RadioResponseResult.Owner(owner) else null } @@ -1268,8 +1294,8 @@ class RadioConfigViewModelTest { // Channel A (index 1) completed before channel B (index 2) threw. assertEquals(listOf(1, 2), writtenIndexes) assertNotNull(interrupted) - assertEquals(1, interrupted!!.appliedWriteCount) - assertEquals("A", interrupted!!.appliedSettings[1].name) + assertEquals(1, interrupted.appliedWriteCount) + assertEquals("A", interrupted.appliedSettings[1].name) } @Test @@ -1296,6 +1322,85 @@ class RadioConfigViewModelTest { verifySuspend { adminActionsUseCase.reboot(123, any()) } } + @Test + fun `setResponseStateLoading for REBOOT_DFU calls useCase after config response`() = runTest { + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) + nodeRepository.setNodes(listOf(node)) + + val packetFlow = MutableSharedFlow() + every { serviceRepository.meshPacketFlow } returns packetFlow + every { processRadioResponseUseCase(any(), any(), any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) + + viewModel = createViewModel() + + everySuspend { adminActionsUseCase.rebootToDfu(any(), any()) } returns 42 + + viewModel.setResponseStateLoading(AdminRoute.REBOOT_DFU) + packetFlow.emit(MeshPacket.Builder().build()) + + verifySuspend { adminActionsUseCase.rebootToDfu(123, any()) } + } + + @Test + fun `an unacknowledged DFU request settles as success instead of a timeout error`() = runTest { + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) + nodeRepository.setNodes(listOf(node)) + + val packetFlow = MutableSharedFlow() + every { serviceRepository.meshPacketFlow } returns packetFlow + every { processRadioResponseUseCase(any(), any(), any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) + + viewModel = createViewModel() + + everySuspend { adminActionsUseCase.rebootToDfu(any(), any()) } calls + { + it.args.onRequestIdArg()(42) + 42 + } + + viewModel.setResponseStateLoading(AdminRoute.REBOOT_DFU) + packetFlow.emit(MeshPacket.Builder().build()) + runCurrent() + verifySuspend { adminActionsUseCase.rebootToDfu(123, any()) } + assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) + + advanceTimeBy(31_000) + runCurrent() + + assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Success) + } + + @Test + fun `canRebootToDfu is true only for nRF52 hardware`() = runTest { + val node = + Node( + num = 123, + user = + User.Builder() + .also { wb -> + wb.id = "!123" + wb.hw_model = HardwareModel.RAK4631 + } + .build(), + ) + nodeRepository.setNodes(listOf(node)) + every { deviceHardwareRepository.observeDeviceHardware(HardwareModel.RAK4631.value, any()) } returns + flowOf(DeviceHardware(architecture = "nrf52840")) + + viewModel = createViewModel(destNum = 123) + advanceUntilIdle() + assertTrue(viewModel.radioConfigState.value.canRebootToDfu) + + every { deviceHardwareRepository.observeDeviceHardware(HardwareModel.RAK4631.value, any()) } returns + flowOf(DeviceHardware(architecture = "esp32-s3")) + + viewModel = createViewModel(destNum = 123) + advanceUntilIdle() + assertFalse(viewModel.radioConfigState.value.canRebootToDfu) + } + @Test fun `setResponseStateLoading for FACTORY_RESET calls useCase after config response`() = runTest { val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) @@ -1919,7 +2024,7 @@ class RadioConfigViewModelTest { var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT) every { processRadioResponseUseCase(any(), 456, any()) } calls { - val pendingRequestIds = it.args[2] as Set + val pendingRequestIds = it.arg>(2) if (42 in pendingRequestIds) response else null } nodeRepository.setNodes(listOf(localNode, remoteNode)) @@ -2035,7 +2140,7 @@ class RadioConfigViewModelTest { Node( num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build(), - metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(), ) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") @@ -2783,6 +2888,48 @@ class RadioConfigViewModelTest { assertFalse(nodeRestartTracker.restartExpected.value) } + + @Test + fun `local module save that reboots opens the restart window`() = runTest { + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) + nodeRepository.setNodes(listOf(node)) + nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) + viewModel = createViewModel() + runCurrent() + everySuspend { radioConfigUseCase.setModuleConfig(any(), any(), any()) } returns 42 + + nodeRestartTracker.onConnected() + viewModel.setModuleConfig( + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build(), + ) + runCurrent() + + assertTrue(nodeRestartTracker.restartExpected.value) + } + + @Test + fun `local Mesh Beacon save does not open the restart window`() = runTest { + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) + nodeRepository.setNodes(listOf(node)) + nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) + viewModel = createViewModel() + runCurrent() + everySuspend { radioConfigUseCase.setModuleConfig(any(), any(), any()) } returns 42 + + nodeRestartTracker.onConnected() + viewModel.setModuleConfig( + ModuleConfig.Builder() + .also { wb -> + wb.mesh_beacon = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_message = "hi" }.build() + } + .build(), + ) + runCurrent() + + assertFalse(nodeRestartTracker.restartExpected.value) + } } /** Extracts the trailing `onRequestId` callback from a mocked request method's args. */ diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt index d2e2a529f2..862587015c 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.test.v2.runComposeUiTest import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.i_agree -import org.meshtastic.core.resources.map_reporting -import org.meshtastic.core.resources.map_reporting_summary +import org.meshtastic.core.resources.schema_mqtt_map_reporting_enabled +import org.meshtastic.core.resources.schema_mqtt_map_reporting_enabled_description import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -62,8 +62,8 @@ class MapReportingPreferenceTest { } } // Verify that the dialog title is displayed - onNodeWithText(getString(Res.string.map_reporting)).assertIsDisplayed() - onNodeWithText(getString(Res.string.map_reporting_summary)).assertIsDisplayed() + onNodeWithText(getString(Res.string.schema_mqtt_map_reporting_enabled)).assertIsDisplayed() + onNodeWithText(getString(Res.string.schema_mqtt_map_reporting_enabled_description)).assertIsDisplayed() } @Test @@ -84,14 +84,14 @@ class MapReportingPreferenceTest { } } onNodeWithText(getString(Res.string.i_agree)).assertDoesNotExist() - onNodeWithText(getString(Res.string.map_reporting)).performClick() + onNodeWithText(getString(Res.string.schema_mqtt_map_reporting_enabled)).performClick() assertFalse(mapReportingEnabled) assertFalse(shouldReportLocation) onNodeWithText(getString(Res.string.i_agree)).assertIsDisplayed() onNodeWithText(getString(Res.string.i_agree)).performClick() assertTrue(shouldReportLocation) assertTrue(mapReportingEnabled) - onNodeWithText(getString(Res.string.map_reporting)).performClick() + onNodeWithText(getString(Res.string.schema_mqtt_map_reporting_enabled)).performClick() onNodeWithText(getString(Res.string.i_agree)).assertDoesNotExist() assertTrue(shouldReportLocation) assertFalse(mapReportingEnabled) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt index 102dff34e4..0cf9b7a6d7 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt @@ -17,6 +17,8 @@ package org.meshtastic.feature.settings.radio.component import okio.ByteString.Companion.encodeUtf8 +import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.numChannels import org.meshtastic.feature.settings.util.FixedUpdateIntervals import org.meshtastic.feature.settings.util.IntervalConfiguration import org.meshtastic.proto.ChannelSettings @@ -640,4 +642,58 @@ class MeshBeaconConfigPolicyTest { assertEquals(listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()), updated) } + + // US, not EU_868: EU_868 is 869.4-869.65 MHz, a single slot, so there would be no second slot to pin to and the + // pinned-radio case below could not be expressed at all. + private val presetRadio = + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = true + } + .build() + private val primaryChannel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() + + @Test + fun stampBeaconConfigForSave_pinnedRadio_advertisesTheSlotItActuallySitsOn() { + assertTrue(presetRadio.numChannels > 1, "a one-slot region makes this vacuous") + val derived = Channel(primaryChannel, presetRadio).channelNum + val pin = if (derived == 1) 2 else 1 + val radioLora = presetRadio.newBuilder().also { wb -> wb.channel_num = pin }.build() + + val stamped = + stampBeaconConfigForSave( + MeshBeaconConfig.Builder().build(), + MeshBeaconConfig.Builder().build(), + radioLora, + channelList = listOf(primaryChannel), + ) + + assertEquals(pin, stamped.broadcast_offer_frequency_slot) + } + + @Test + fun stampBeaconConfigForSave_derivableSlot_advertisesNothing() { + val stamped = + stampBeaconConfigForSave( + MeshBeaconConfig.Builder().build(), + MeshBeaconConfig.Builder().build(), + presetRadio, + channelList = listOf(primaryChannel), + ) + + // A receiver works this slot out from the offer itself, so spending bytes on it would be waste. + assertNull(stamped.broadcast_offer_frequency_slot) + } + + @Test + fun stampBeaconConfigForSave_customLoraParams_preservesTheStoredSlot() { + val radioLora = presetRadio.newBuilder().also { wb -> wb.use_preset = false }.build() + val stored = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_offer_frequency_slot = 48 }.build() + + val stamped = stampBeaconConfigForSave(MeshBeaconConfig.Builder().build(), stored, radioLora, emptyList()) + + assertEquals(48, stamped.broadcast_offer_frequency_slot) + } } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MqttTlsPreferenceTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MqttTlsPreferenceTest.kt new file mode 100644 index 0000000000..868938ccc1 --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MqttTlsPreferenceTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.radio.component + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.tls_enabled_public_broker_summary +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.proto.ModuleConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * What the switch shows must be what gets stored: the radio uses the stored `tls_enabled` verbatim when it reaches the + * broker over its own network, so a display that disagrees with the stored flag is a security defect, not cosmetics. + */ +@OptIn(ExperimentalTestApi::class) +class MqttTlsPreferenceTest { + + /** Renders the preference over a real MQTTConfig so the assertions are about the config that would be sent. */ + private fun storedConfig(address: String, tlsEnabled: Boolean) = ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.address = address + wb.tls_enabled = tlsEnabled + } + .build() + + @Test + fun `public broker with TLS off shows the switch off and settable - stored config is the truth`() = + runComposeUiTest { + // The empty address is the public broker, and the shipping default. + var config by mutableStateOf(storedConfig(address = "", tlsEnabled = false)) + setContent { + AppTheme { + MqttTlsPreference( + enabled = true, + address = config.address, + tlsEnabled = config.tls_enabled, + onCheckedChange = { config = config.newBuilder().also { wb -> wb.tls_enabled = it }.build() }, + ) + } + } + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOff().assertIsEnabled() + assertFalse(config.tls_enabled) + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).performClick() + + // The user can now store TLS for a radio that connects to the public broker by itself. + assertTrue(config.tls_enabled) + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOn() + } + + @Test + fun `explicit public broker address with TLS off shows the switch off`() = runComposeUiTest { + setContent { + AppTheme { + MqttTlsPreference( + enabled = true, + address = "mqtt.meshtastic.org", + tlsEnabled = false, + onCheckedChange = {}, + ) + } + } + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOff().assertIsEnabled() + } + + @Test + fun `stored TLS on shows the switch on`() = runComposeUiTest { + val config = storedConfig(address = "mqtt.example.org", tlsEnabled = true) + setContent { + AppTheme { + MqttTlsPreference( + enabled = true, + address = config.address, + tlsEnabled = config.tls_enabled, + onCheckedChange = {}, + ) + } + } + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOn() + } + + @Test + fun `custom broker turns TLS on and off without interference`() = runComposeUiTest { + var config by mutableStateOf(storedConfig(address = "mqtt.example.org", tlsEnabled = true)) + setContent { + AppTheme { + MqttTlsPreference( + enabled = true, + address = config.address, + tlsEnabled = config.tls_enabled, + onCheckedChange = { config = config.newBuilder().also { wb -> wb.tls_enabled = it }.build() }, + ) + } + } + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).performClick() + + assertFalse(config.tls_enabled) + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOff() + } + + @Test + fun `public broker explains the phone-relay TLS upgrade`() = runComposeUiTest { + setContent { + AppTheme { MqttTlsPreference(enabled = true, address = "", tlsEnabled = false, onCheckedChange = {}) } + } + + onNodeWithText(getString(Res.string.tls_enabled_public_broker_summary)).assertIsDisplayed() + } + + @Test + fun `custom broker shows no phone-relay note`() = runComposeUiTest { + setContent { + AppTheme { + MqttTlsPreference( + enabled = true, + address = "mqtt.example.org", + tlsEnabled = false, + onCheckedChange = {}, + ) + } + } + + onNodeWithText(getString(Res.string.tls_enabled_public_broker_summary)).assertDoesNotExist() + } + + @Test + fun `a disconnected radio leaves the switch untouchable but still truthful`() = runComposeUiTest { + var config by mutableStateOf(storedConfig(address = "", tlsEnabled = false)) + setContent { + AppTheme { + MqttTlsPreference( + enabled = false, + address = config.address, + tlsEnabled = config.tls_enabled, + onCheckedChange = { config = config.newBuilder().also { wb -> wb.tls_enabled = it }.build() }, + ) + } + } + + onNodeWithTag(MQTT_TLS_SWITCH_TEST_TAG).assertIsOff() + assertEquals(false, config.tls_enabled) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalogTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalogTest.kt new file mode 100644 index 0000000000..6cc948386f --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchCatalogTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.allStringResources +import org.meshtastic.feature.settings.navigation.ConfigRoute +import org.meshtastic.feature.settings.navigation.ModuleRoute +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * The catalog names which settings screen owns each schema message. Nothing in the schema carries that mapping, so it + * is hand-written, and these tests are what keep it honest when the schema moves under it. + */ +class SettingsSearchCatalogTest { + + private val schemaKeys: Set = Res.allStringResources.keys.filter { it.startsWith("schema_") }.toSet() + + /** `schema_lora_hop_limit` and `schema_lora_modempreset_long_fast` both reduce to `lora`. */ + private fun messagePrefixes(): Set = + schemaKeys.mapNotNull { it.removePrefix("schema_").substringBefore('_').ifEmpty { null } }.toSet() + + @Test + fun everyLabelledMessageIsEitherSearchableOrExcused() { + val claimed = SettingsSearchCatalog.declaredMessagePrefixes() + val excused = SettingsSearchCatalog.excusedMessagePrefixes() + val unaccounted = messagePrefixes() - claimed - excused + + assertTrue( + unaccounted.isEmpty(), + "the schema labels fields on these messages and nothing says which settings screen owns them: " + + unaccounted.sorted().joinToString() + + ". Add the message to schemaPrefixes, or to notSearchable with a reason.", + ) + } + + /** + * The generated set is pinned against the registry upstream; this checks the other half, that every prefix in it + * reaches a resource this module can actually see, which is what the index filters on. + */ + @Test + fun everyDeclaredEnumStillHasValuesInTheSchema() { + val stale = + SettingsSearchCatalog.declaredEnumValuePrefixes().filter { prefix -> + schemaKeys.none { it.startsWith(prefix) } + } + + assertEquals( + emptyList(), + stale.sorted(), + "these enum prefixes match no string resource, so the index is filtering on something that is not there. " + + "The generated set and schema_strings.xml have diverged; re-run :schema-strings:sync.", + ) + } + + @Test + fun noEnumValueIsOfferedAsASetting() { + val enumPrefixes = SettingsSearchCatalog.declaredEnumValuePrefixes() + val titles = SettingsSearchCatalog.entries().mapNotNull { entry -> keyOf(entry.title) } + val leaked = titles.filter { key -> enumPrefixes.any { key.startsWith(it) } } + + assertEquals(emptyList(), leaked.sorted(), "these are picker options, not settings, and must not be indexed") + } + + @Test + fun noDescriptionIsOfferedAsASetting() { + val leaked = + SettingsSearchCatalog.entries().mapNotNull { keyOf(it.title) }.filter { it.endsWith("_description") } + + assertEquals(emptyList(), leaked.sorted(), "a field's explanation is its subtitle, never a result of its own") + } + + @Test + fun everyEntryHasADistinctId() { + val duplicates = + SettingsSearchCatalog.entries().groupingBy { it.id }.eachCount().filterValues { it > 1 }.keys.sorted() + + assertEquals(emptyList(), duplicates, "the results list keys on the id, and a duplicate key crashes it") + } + + @Test + fun everyConfigurationScreenIsReachableFromSearch() { + val routes = SettingsSearchCatalog.entries().map { it.route }.toSet() + val missing = (ConfigRoute.entries.map { it.route } + ModuleRoute.entries.map { it.route }) - routes + + assertEquals(emptyList(), missing, "these settings screens cannot be found by searching for their own name") + } + + @Test + fun onlyThisPhonesOwnSettingsAreMarkedAppLocal() { + val entries = SettingsSearchCatalog.entries() + val appLocal = entries.filter { it.isAppLocal } + + // A remote session drops these, so a proto-backed control must never be caught by that filter. + assertTrue(appLocal.isNotEmpty(), "the app's own settings should be searchable on a local session") + assertTrue( + appLocal.none { entry -> keyOf(entry.title)?.startsWith("schema_") == true }, + "a radio setting was marked app-local and would vanish during remote administration", + ) + } + + /** The resource's registered name, which is what the catalog keys off. */ + private fun keyOf(resource: StringResource): String? = + Res.allStringResources.entries.firstOrNull { it.value == resource }?.key +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcherTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcherTest.kt new file mode 100644 index 0000000000..ad6b77923f --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/search/SettingsSearchMatcherTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.search + +import org.meshtastic.core.navigation.SettingsRoute +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SettingsSearchMatcherTest { + + private fun entry(title: String, description: String? = null, screen: String = "LoRa") = ResolvedSettingsEntry( + id = title, + title = title, + description = description, + screenTitle = screen, + route = SettingsRoute.LoRa, + ) + + @Test + fun blankQueryMatchesNothing() { + assertEquals(emptyList(), SettingsSearchMatcher.rank(listOf(entry("Hop limit")), " ")) + } + + @Test + fun anExactNameOutranksAPrefixWhichOutranksAMentionInTheExplanation() { + val entries = + listOf( + entry("Rebroadcast mode", description = "How this node handles hops"), + entry("Hops"), + entry("Hop limit"), + ) + + val titles = SettingsSearchMatcher.rank(entries, "hops").map { it.title } + + assertEquals(listOf("Hops", "Rebroadcast mode"), titles) + } + + @Test + fun aNameAndExplanationHitOutranksANameHitAlone() { + val both = entry("Hop limit", description = "The maximum hop limit to use") + val nameOnly = entry("Hop behaviour") + + val ranked = SettingsSearchMatcher.rank(listOf(nameOnly, both), "hop limit") + + assertEquals("Hop limit", ranked.first().title) + } + + @Test + fun matchingFoldsCase() { + assertTrue(SettingsSearchMatcher.rank(listOf(entry("LoRa region")), "lora").isNotEmpty()) + assertTrue(SettingsSearchMatcher.rank(listOf(entry("lora region")), "LORA").isNotEmpty()) + } + + @Test + fun theScreenNameIsMatchedSoAControlIsFoundByItsScreen() { + val ranked = SettingsSearchMatcher.rank(listOf(entry("Enabled", screen = "Bluetooth")), "bluetooth") + + assertEquals(listOf("Enabled"), ranked.map { it.title }) + } + + @Test + fun equalScoresOrderByNameSoResultsDoNotShuffle() { + val entries = listOf(entry("Zulu mode"), entry("Alpha mode"), entry("Mike mode")) + + val titles = SettingsSearchMatcher.rank(entries, "mode").map { it.title } + + assertEquals(listOf("Alpha mode", "Mike mode", "Zulu mode"), titles) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnitsTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnitsTest.kt new file mode 100644 index 0000000000..b714a4a3cc --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataUnitsTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.util + +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.unit_dbm +import org.meshtastic.core.resources.unit_khz +import org.meshtastic.core.resources.unit_meters +import org.meshtastic.proto.Config +import org.meshtastic.proto.FieldMetadata +import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.bandwidth +import org.meshtastic.proto.ble_threshold +import org.meshtastic.proto.broadcast_smart_minimum_distance +import org.meshtastic.proto.tx_power +import org.meshtastic.proto.wifi_threshold +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class FieldMetadataUnitsTest { + + @Test + fun theUnitComesFromTheSchema_notTheCallSite() { + assertEquals(Res.string.unit_dbm, Config.LoRaConfig.tx_power.unitLabelRes) + assertEquals(Res.string.unit_dbm, ModuleConfig.PaxcounterConfig.ble_threshold.unitLabelRes) + assertEquals(Res.string.unit_dbm, ModuleConfig.PaxcounterConfig.wifi_threshold.unitLabelRes) + assertEquals(Res.string.unit_meters, Config.PositionConfig.broadcast_smart_minimum_distance.unitLabelRes) + assertEquals(Res.string.unit_khz, Config.LoRaConfig.bandwidth.unitLabelRes) + } + + @Test + fun aFieldWithNoUnit_namesNone() { + assertNull(FieldMetadata.Builder().build().unitLabelRes) + } + + @Test + fun aSymbolTheAppDoesNotName_rendersNothingRatherThanTheSymbol() { + assertNull(FieldMetadata.Builder().unit("furlong").build().unitLabelRes) + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurationsTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurationsTest.kt new file mode 100644 index 0000000000..5481961717 --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FixedOutputDurationsTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FixedOutputDurationsTest { + + @Test + fun aDurationLabelledInSecondsIsWorthAThousandMilliseconds() { + assertEquals(1000L, FixedOutputDurations.ONE_SECOND.value) + assertEquals(5000L, FixedOutputDurations.FIVE_SECONDS.value) + assertEquals(10_000L, FixedOutputDurations.TEN_SECONDS.value) + } + + @Test + fun theDurationsFirmwareDefaultsToAreOffered() { + val offered = FixedOutputDurations.allowed.map { it.value } + + // NodeDB.cpp seeds output_ms at 100, 500 or 1000 depending on the board, so a radio nobody has configured + // still matches a row here rather than reading as unset. + assertTrue(offered.containsAll(listOf(100L, 500L, 1000L)), "firmware defaults missing from $offered") + } + + @Test + fun noDurationIsShorterThanFirmwareWouldDrive() { + val shortest = FixedOutputDurations.allowed.map { it.value }.filter { it > 0L }.min() + + assertEquals(100L, shortest) + } +} diff --git a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/debugging/NoopStubs.kt b/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/debugging/NoopStubs.kt index b47f97d702..820a0ad6ce 100644 --- a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/debugging/NoopStubs.kt +++ b/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/debugging/NoopStubs.kt @@ -16,8 +16,4 @@ */ package org.meshtastic.feature.settings.debugging -import androidx.compose.runtime.Composable - -@Composable actual fun rememberLogExporter(contentProvider: suspend () -> String): (fileName: String) -> Unit = { _ -> } - actual fun captureAppLogcat(): String = "" diff --git a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt b/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt index 54b29f2e70..6b97fd18e2 100644 --- a/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt +++ b/feature/settings/src/iosMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt @@ -18,8 +18,11 @@ package org.meshtastic.feature.settings.tak import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState @Composable actual fun TakPermissionHandler(isTakServerEnabled: Boolean, onPermissionResult: (Boolean) -> Unit) { - LaunchedEffect(isTakServerEnabled) { onPermissionResult(true) } + val currentOnPermissionResult by rememberUpdatedState(onPermissionResult) + LaunchedEffect(isTakServerEnabled) { currentOnPermissionResult(true) } } diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt index cb9e8b92db..92ed8db30c 100644 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt @@ -25,18 +25,15 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.common.util.UnitsOverride import org.meshtastic.core.navigation.DiscoveryRoute import org.meshtastic.core.navigation.Route @@ -45,22 +42,17 @@ import org.meshtastic.core.navigation.WifiProvisionRoute import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.about import org.meshtastic.core.resources.app_settings -import org.meshtastic.core.resources.app_version import org.meshtastic.core.resources.bottom_nav_settings import org.meshtastic.core.resources.device_links import org.meshtastic.core.resources.discovery_local_mesh import org.meshtastic.core.resources.help_and_documentation import org.meshtastic.core.resources.info -import org.meshtastic.core.resources.modules_already_unlocked -import org.meshtastic.core.resources.modules_unlocked import org.meshtastic.core.resources.node_layout_section_title import org.meshtastic.core.resources.preferences_language -import org.meshtastic.core.resources.remotely_administrating import org.meshtastic.core.resources.theme import org.meshtastic.core.resources.units import org.meshtastic.core.resources.wifi_devices import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.icon.ChevronRight import org.meshtastic.core.ui.icon.Device @@ -70,16 +62,16 @@ import org.meshtastic.core.ui.icon.HelpOutline import org.meshtastic.core.ui.icon.Info import org.meshtastic.core.ui.icon.Language import org.meshtastic.core.ui.icon.List -import org.meshtastic.core.ui.icon.Memory import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.PermScanWifi import org.meshtastic.core.ui.icon.Wifi -import org.meshtastic.core.ui.util.rememberShowToastResource +import org.meshtastic.feature.settings.component.AppVersionButton import org.meshtastic.feature.settings.component.CacheLimitPreference import org.meshtastic.feature.settings.component.ExpressiveSection import org.meshtastic.feature.settings.component.FullMessageTimestampsSetting import org.meshtastic.feature.settings.component.HomoglyphSetting import org.meshtastic.feature.settings.component.NotificationSection +import org.meshtastic.feature.settings.component.RadioAdminAppBar import org.meshtastic.feature.settings.component.ThemePickerDialog import org.meshtastic.feature.settings.component.UnitsOption import org.meshtastic.feature.settings.component.UnitsPickerDialog @@ -87,7 +79,8 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute import org.meshtastic.feature.settings.radio.RadioConfigItemList import org.meshtastic.feature.settings.radio.RadioConfigViewModel -import kotlin.time.Duration.Companion.seconds +import org.meshtastic.feature.settings.search.SettingsSearchBar +import org.meshtastic.feature.settings.search.SettingsSearchViewModel /** * Desktop-specific top-level settings screen. Replaces the Android `SettingsScreen` which uses Android-specific APIs @@ -139,21 +132,13 @@ fun DesktopSettingsScreen( Scaffold( topBar = { - MainAppBar( + RadioAdminAppBar( title = stringResource(Res.string.bottom_nav_settings), - subtitle = - if (state.isLocal) { - null - } else { - val remoteName = destNode?.user?.long_name ?: "" - stringResource(Res.string.remotely_administrating, remoteName) - }, - ourNode = null, - showNodeChip = false, - canNavigateUp = false, + isLocal = state.isLocal, + destNode = destNode, onNavigateUp = {}, - actions = {}, - onClickChip = {}, + localSubtitle = null, + canNavigateUp = false, ) }, ) { paddingValues -> @@ -161,6 +146,13 @@ fun DesktopSettingsScreen( modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState()).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { + SettingsSearchBar( + viewModel = koinViewModel(), + onNavigate = onNavigate, + // This phone's own settings are hidden below while administering another node; search hides them too. + includeAppLocal = state.isLocal, + ) + RadioConfigItemList( state = state, isManaged = localConfig.security?.is_managed ?: false, @@ -305,7 +297,7 @@ private fun DesktopAppInfoSection( onNavigateToAbout() } - DesktopAppVersionButton( + AppVersionButton( hiddenFeaturesUnlocked = hiddenFeaturesUnlocked, appVersionName = appVersionName, onUnlockHiddenFeatures = onUnlockHiddenFeatures, @@ -313,50 +305,6 @@ private fun DesktopAppInfoSection( } } -private const val UNLOCK_CLICK_COUNT = 5 -private const val UNLOCKED_CLICK_COUNT = 3 -private const val UNLOCK_TIMEOUT_SECONDS = 1 - -@Composable -private fun DesktopAppVersionButton( - hiddenFeaturesUnlocked: Boolean, - appVersionName: String, - onUnlockHiddenFeatures: () -> Unit, -) { - val scope = rememberCoroutineScope() - val showToast = rememberShowToastResource() - var clickCount by remember { mutableStateOf(0) } - - LaunchedEffect(clickCount) { - if (clickCount in 1.. { - clickCount = 0 - scope.launch { showToast(Res.string.modules_already_unlocked) } - } - - clickCount == UNLOCK_CLICK_COUNT -> { - clickCount = 0 - onUnlockHiddenFeatures() - scope.launch { showToast(Res.string.modules_unlocked) } - } - } - } -} - /** * Supported languages — tag must match the CMP `values-` directory names. Empty tag means system default. * Display names are written in the native language for clarity. diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt index 817536abfa..47107a947c 100644 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -16,58 +16,7 @@ */ package org.meshtastic.feature.settings.debugging -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import co.touchlab.kermit.Logger -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.meshtastic.core.common.log.InMemoryLogBuffer -import org.meshtastic.core.common.util.ioDispatcher -import java.awt.FileDialog -import java.awt.Frame -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStreamWriter -import java.nio.charset.StandardCharsets - -@Composable -actual fun rememberLogExporter(contentProvider: suspend () -> String): (fileName: String) -> Unit { - val scope = rememberCoroutineScope() - - return { fileName -> - scope.launch { - val content = contentProvider() - if (content.isBlank()) { - Logger.w { "Log export aborted: no content" } - return@launch - } - - withContext(ioDispatcher) { - // Run file dialog to ask user where to save - val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE) - fileDialog.file = fileName - fileDialog.isVisible = true - - val directory = fileDialog.directory - val selectedFile = fileDialog.file - - if (directory != null && selectedFile != null) { - val exportFile = File(directory, selectedFile) - try { - FileOutputStream(exportFile).use { fos -> - OutputStreamWriter(fos, StandardCharsets.UTF_8).use { writer -> writer.write(content) } - } - Logger.i { "Logs exported successfully to ${exportFile.absolutePath}" } - } catch (e: java.io.IOException) { - Logger.e(e) { "Failed to export logs to file: ${exportFile.absolutePath}" } - } - } else { - Logger.w { "Log export aborted: user canceled file dialog" } - } - } - } - } -} // Desktop has no system logcat; surface the app's own Kermit output captured by InMemoryLogBuffer (installed at // startup). diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.jvm.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.jvm.kt index 0c7ecac040..d337938742 100644 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.jvm.kt +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.jvm.kt @@ -18,128 +18,7 @@ package org.meshtastic.feature.settings.radio.component import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import org.meshtastic.core.model.util.toPosixString import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.time.zone.ZoneOffsetTransitionRule -import java.util.Locale -import kotlin.math.abs @Composable actual fun rememberSystemTimeZonePosixString(): String = remember { ZoneId.systemDefault().toPosixString() } - -/** Generates a POSIX time zone string from a [ZoneId]. JVM/Desktop version of the Android-only `core:model` utility. */ -@Suppress("MagicNumber", "ReturnCount") -private fun ZoneId.toPosixString(): String { - val rules = this.rules - - if (rules.isFixedOffset || rules.transitionRules.isEmpty()) { - val now = java.time.Instant.now() - val zdt = ZonedDateTime.ofInstant(now, this) - return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}" - } - - val springRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds > it.offsetBefore.totalSeconds } - val fallRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds < it.offsetBefore.totalSeconds } - - if (springRule == null || fallRule == null) { - val now = java.time.Instant.now() - val zdt = ZonedDateTime.ofInstant(now, this) - return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}" - } - - return buildString { - val stdAbbrev = getTransitionAbbreviation(this@toPosixString, fallRule) - val dstAbbrev = getTransitionAbbreviation(this@toPosixString, springRule) - - append(formatAbbreviation(stdAbbrev)) - append(formatPosixOffset(springRule.offsetBefore)) - append(formatAbbreviation(dstAbbrev)) - - if (springRule.offsetAfter.totalSeconds - springRule.offsetBefore.totalSeconds != 3600) { - append(formatPosixOffset(springRule.offsetAfter)) - } - - append(formatTransitionRule(springRule)) - append(formatTransitionRule(fallRule)) - } -} - -private fun ZonedDateTime.timeZoneShortName(): String { - val formatter = DateTimeFormatter.ofPattern("zzz", Locale.ENGLISH) - val shortName = format(formatter) - return if (shortName.startsWith("GMT")) "GMT" else shortName -} - -private fun formatAbbreviation(abbrev: String): String = if (abbrev.all { it.isLetter() }) abbrev else "<$abbrev>" - -private fun getTransitionAbbreviation(zone: ZoneId, rule: ZoneOffsetTransitionRule): String { - val year = java.time.LocalDate.now().year - val transition = rule.createTransition(year) - return ZonedDateTime.ofInstant(transition.instant, zone).timeZoneShortName() -} - -@Suppress("MagicNumber") -private fun formatPosixOffset(offset: ZoneOffset): String { - val offsetSeconds = -offset.totalSeconds - val hours = offsetSeconds / 3600 - val remainingSeconds = abs(offsetSeconds) % 3600 - val minutes = remainingSeconds / 60 - val seconds = remainingSeconds % 60 - - return buildString { - if (offsetSeconds < 0 && hours == 0) append("-") - append(hours) - if (minutes != 0 || seconds != 0) { - append(":%02d".format(Locale.ENGLISH, minutes)) - if (seconds != 0) { - append(":%02d".format(Locale.ENGLISH, seconds)) - } - } - } -} - -@Suppress("MagicNumber") -private fun formatTransitionRule(rule: ZoneOffsetTransitionRule): String { - val month = rule.month.value - val dayOfWeek = rule.dayOfWeek.value % 7 - val dayIndicator = rule.dayOfMonthIndicator - - val occurrence = - when { - dayIndicator < 0 -> 5 - dayIndicator > rule.month.length(false) - 7 -> 5 - else -> ((dayIndicator - 1) / 7) + 1 - } - - val wallTime = - when (rule.timeDefinition) { - ZoneOffsetTransitionRule.TimeDefinition.UTC -> - rule.localTime.plusSeconds(rule.offsetBefore.totalSeconds.toLong()) - - ZoneOffsetTransitionRule.TimeDefinition.STANDARD -> { - if (rule.offsetAfter.totalSeconds > rule.offsetBefore.totalSeconds) { - rule.localTime - } else { - rule.localTime.plusSeconds( - (rule.offsetBefore.totalSeconds - rule.offsetAfter.totalSeconds).toLong(), - ) - } - } - - else -> rule.localTime - } - - return buildString { - append(",M$month.$occurrence.$dayOfWeek") - if (wallTime.hour != 2 || wallTime.minute != 0 || wallTime.second != 0) { - append("/${wallTime.hour}") - if (wallTime.minute != 0 || wallTime.second != 0) { - append(":%02d".format(Locale.ENGLISH, wallTime.minute)) - if (wallTime.second != 0) { - append(":%02d".format(Locale.ENGLISH, wallTime.second)) - } - } - } - } -} diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt deleted file mode 100644 index bfbb85bc0d..0000000000 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.settings.tak - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import co.touchlab.kermit.Logger -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.meshtastic.core.common.util.ioDispatcher -import java.awt.FileDialog -import java.awt.Frame -import java.io.File - -@Composable -actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit { - val scope = rememberCoroutineScope() - return { fileName -> - scope.launch { - runCatching { - val fileDialog = - FileDialog(null as Frame?, "Export TAK Data Package", FileDialog.SAVE).apply { - file = fileName - isVisible = true - } - - val directory = fileDialog.directory - val file = fileDialog.file - - if (directory != null && file != null) { - val targetFile = File(directory, file) - val data = dataPackageProvider() - withContext(ioDispatcher) { targetFile.writeBytes(data) } - Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" } - } - } - .onFailure { e -> Logger.e(e) { "Failed to export TAK data package" } } - } - } -} diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt index 54b29f2e70..6b97fd18e2 100644 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/TakPermissionUtil.kt @@ -18,8 +18,11 @@ package org.meshtastic.feature.settings.tak import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState @Composable actual fun TakPermissionHandler(isTakServerEnabled: Boolean, onPermissionResult: (Boolean) -> Unit) { - LaunchedEffect(isTakServerEnabled) { onPermissionResult(true) } + val currentOnPermissionResult by rememberUpdatedState(onPermissionResult) + LaunchedEffect(isTakServerEnabled) { currentOnPermissionResult(true) } } diff --git a/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMergeCoverageTest.kt b/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMergeCoverageTest.kt new file mode 100644 index 0000000000..f2a6c60388 --- /dev/null +++ b/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/ModuleConfigMergeCoverageTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.radio + +import com.squareup.wire.ProtoAdapter +import com.squareup.wire.WireField +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** Walks Wire's generated oneof, so a variant the proto grows fails here by name. JVM-only: reads `@WireField`. */ +class ModuleConfigMergeCoverageTest { + + @Test + fun `merging every ModuleConfig variant in turn fills and keeps every LocalModuleConfig section`() { + val variants = + ModuleConfig::class.java.declaredFields.filter { + it.getAnnotation(WireField::class.java)?.oneofName == "payload_variant" + } + assertTrue(variants.isNotEmpty(), "found no @WireField fields in the ModuleConfig payload_variant oneof") + assertTrue(variants.any { it.name == "mesh_beacon" }, "mesh_beacon is not among the oneof fields found") + + val merged = + variants.fold(LocalModuleConfig.Builder().build()) { acc, field -> + val value = (field.type.getField("ADAPTER").get(null) as ProtoAdapter<*>).decode(ByteArray(0)) + acc.mergedWith(ModuleConfig.Builder().also { it.javaClass.getField(field.name).set(it, value) }.build()) + } + + val dropped = variants.map { it.name }.filter { LocalModuleConfig::class.java.getField(it).get(merged) == null } + assertEquals(emptyList(), dropped, "these variants are never merged, or a later merge cleared them") + } +} diff --git a/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/SchemaFieldCoverageTest.kt b/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/SchemaFieldCoverageTest.kt new file mode 100644 index 0000000000..78d474ebd0 --- /dev/null +++ b/feature/settings/src/jvmTest/kotlin/org/meshtastic/feature/settings/radio/SchemaFieldCoverageTest.kt @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.radio + +import com.squareup.wire.WireField +import org.meshtastic.proto.FieldMetadataRegistry +import java.io.File +import java.util.zip.ZipFile +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Every field the schema gives a label is either offered by a settings screen or named below with a reason. A field the + * schema does not label cannot be rendered at all - there is no text for it - so it is never listed. + * + * What this pins is the schema side, which is where the change comes from: a newly annotated field upstream moves the + * count and has to be given a control or a reason. It cannot prove a screen renders a field; android lays its screens + * out by hand, so unlike Meshtastic-Apple's overlay there is no structure to read that from. The reasons below are a + * review matter, the count is the ratchet. + * + * JVM-only: walking the generated messages needs `Class.forName` over the protobufs jar. + */ +class SchemaFieldCoverageTest { + + private enum class Reason { + /** The field is one android has no control for, on a screen it otherwise renders. */ + NO_CONTROL, + + /** The whole module has no settings screen yet. */ + NO_SCREEN, + } + + private val notOffered = + mapOf( + "Config.LoRaConfig.ignore_incoming" to Reason.NO_CONTROL, + "ModuleConfig.TrafficManagementConfig.nodeinfo_direct_response_max_hops" to Reason.NO_SCREEN, + "ModuleConfig.TrafficManagementConfig.position_min_interval_secs" to Reason.NO_SCREEN, + "ModuleConfig.TrafficManagementConfig.rate_limit_max_packets" to Reason.NO_SCREEN, + "ModuleConfig.TrafficManagementConfig.rate_limit_window_secs" to Reason.NO_SCREEN, + "ModuleConfig.TrafficManagementConfig.unknown_packet_threshold" to Reason.NO_SCREEN, + ) + + /** + * Every field the schema labels, as of the pin in `gradle/libs.versions.toml`. A field added upstream is absent + * here and fails the test by name, so the commit that adds it also has to give it a control or a [notOffered] + * reason. The list is regenerated by reading the failure message, not written by hand. + */ + private val accountedFor = + setOf( + "Config.BluetoothConfig.enabled", + "Config.BluetoothConfig.fixed_pin", + "Config.BluetoothConfig.mode", + "Config.DeviceConfig.button_gpio", + "Config.DeviceConfig.buzzer_gpio", + "Config.DeviceConfig.disable_triple_click", + "Config.DeviceConfig.double_tap_as_button_press", + "Config.DeviceConfig.led_heartbeat_disabled", + "Config.DeviceConfig.node_info_broadcast_secs", + "Config.DeviceConfig.rebroadcast_mode", + "Config.DeviceConfig.role", + "Config.DeviceConfig.tzdef", + "Config.DisplayConfig.auto_screen_carousel_secs", + "Config.DisplayConfig.compass_north_top", + "Config.DisplayConfig.compass_orientation", + "Config.DisplayConfig.displaymode", + "Config.DisplayConfig.flip_screen", + "Config.DisplayConfig.heading_bold", + "Config.DisplayConfig.oled", + "Config.DisplayConfig.screen_on_secs", + "Config.DisplayConfig.units", + "Config.DisplayConfig.use_12h_clock", + "Config.DisplayConfig.wake_on_tap_or_motion", + "Config.LoRaConfig.bandwidth", + "Config.LoRaConfig.channel_num", + "Config.LoRaConfig.coding_rate", + "Config.LoRaConfig.config_ok_to_mqtt", + "Config.LoRaConfig.hop_limit", + "Config.LoRaConfig.ignore_incoming", + "Config.LoRaConfig.ignore_mqtt", + "Config.LoRaConfig.modem_preset", + "Config.LoRaConfig.override_duty_cycle", + "Config.LoRaConfig.override_frequency", + "Config.LoRaConfig.pa_fan_disabled", + "Config.LoRaConfig.region", + "Config.LoRaConfig.spread_factor", + "Config.LoRaConfig.sx126x_rx_boosted_gain", + "Config.LoRaConfig.tx_enabled", + "Config.LoRaConfig.tx_power", + "Config.LoRaConfig.use_preset", + "Config.NetworkConfig.IpV4Config.dns", + "Config.NetworkConfig.IpV4Config.gateway", + "Config.NetworkConfig.IpV4Config.ip", + "Config.NetworkConfig.IpV4Config.subnet", + "Config.NetworkConfig.address_mode", + "Config.NetworkConfig.enabled_protocols", + "Config.NetworkConfig.eth_enabled", + "Config.NetworkConfig.ntp_server", + "Config.NetworkConfig.rsyslog_server", + "Config.NetworkConfig.wifi_enabled", + "Config.NetworkConfig.wifi_psk", + "Config.NetworkConfig.wifi_ssid", + "Config.PositionConfig.broadcast_smart_minimum_distance", + "Config.PositionConfig.broadcast_smart_minimum_interval_secs", + "Config.PositionConfig.fixed_position", + "Config.PositionConfig.gps_en_gpio", + "Config.PositionConfig.gps_mode", + "Config.PositionConfig.gps_update_interval", + "Config.PositionConfig.position_broadcast_secs", + "Config.PositionConfig.position_broadcast_smart_enabled", + "Config.PositionConfig.position_flags", + "Config.PositionConfig.rx_gpio", + "Config.PositionConfig.tx_gpio", + "Config.PowerConfig.adc_multiplier_override", + "Config.PowerConfig.is_power_saving", + "Config.PowerConfig.on_battery_shutdown_after_secs", + "Config.PowerConfig.wait_bluetooth_secs", + "Config.SecurityConfig.admin_key", + "Config.SecurityConfig.debug_log_api_enabled", + "Config.SecurityConfig.is_managed", + "Config.SecurityConfig.private_key", + "Config.SecurityConfig.public_key", + "Config.SecurityConfig.serial_enabled", + "ModuleConfig.AmbientLightingConfig.blue", + "ModuleConfig.AmbientLightingConfig.current", + "ModuleConfig.AmbientLightingConfig.green", + "ModuleConfig.AmbientLightingConfig.led_state", + "ModuleConfig.AmbientLightingConfig.red", + "ModuleConfig.AudioConfig.bitrate", + "ModuleConfig.AudioConfig.codec2_enabled", + "ModuleConfig.AudioConfig.i2s_din", + "ModuleConfig.AudioConfig.i2s_sck", + "ModuleConfig.AudioConfig.i2s_sd", + "ModuleConfig.AudioConfig.i2s_ws", + "ModuleConfig.AudioConfig.ptt_pin", + "ModuleConfig.CannedMessageConfig.inputbroker_event_ccw", + "ModuleConfig.CannedMessageConfig.inputbroker_event_cw", + "ModuleConfig.CannedMessageConfig.inputbroker_event_press", + "ModuleConfig.CannedMessageConfig.inputbroker_pin_a", + "ModuleConfig.CannedMessageConfig.inputbroker_pin_b", + "ModuleConfig.CannedMessageConfig.inputbroker_pin_press", + "ModuleConfig.CannedMessageConfig.rotary1_enabled", + "ModuleConfig.CannedMessageConfig.send_bell", + "ModuleConfig.CannedMessageConfig.updown1_enabled", + "ModuleConfig.DetectionSensorConfig.detection_trigger_type", + "ModuleConfig.DetectionSensorConfig.enabled", + "ModuleConfig.DetectionSensorConfig.minimum_broadcast_secs", + "ModuleConfig.DetectionSensorConfig.monitor_pin", + "ModuleConfig.DetectionSensorConfig.name", + "ModuleConfig.DetectionSensorConfig.send_bell", + "ModuleConfig.DetectionSensorConfig.state_broadcast_secs", + "ModuleConfig.DetectionSensorConfig.use_pullup", + "ModuleConfig.ExternalNotificationConfig.active", + "ModuleConfig.ExternalNotificationConfig.alert_bell", + "ModuleConfig.ExternalNotificationConfig.alert_bell_buzzer", + "ModuleConfig.ExternalNotificationConfig.alert_bell_vibra", + "ModuleConfig.ExternalNotificationConfig.alert_message", + "ModuleConfig.ExternalNotificationConfig.alert_message_buzzer", + "ModuleConfig.ExternalNotificationConfig.alert_message_vibra", + "ModuleConfig.ExternalNotificationConfig.enabled", + "ModuleConfig.ExternalNotificationConfig.nag_timeout", + "ModuleConfig.ExternalNotificationConfig.output", + "ModuleConfig.ExternalNotificationConfig.output_buzzer", + "ModuleConfig.ExternalNotificationConfig.output_ms", + "ModuleConfig.ExternalNotificationConfig.output_vibra", + "ModuleConfig.ExternalNotificationConfig.use_i2s_as_buzzer", + "ModuleConfig.ExternalNotificationConfig.use_pwm", + "ModuleConfig.MQTTConfig.address", + "ModuleConfig.MQTTConfig.enabled", + "ModuleConfig.MQTTConfig.encryption_enabled", + "ModuleConfig.MQTTConfig.map_reporting_enabled", + "ModuleConfig.MQTTConfig.password", + "ModuleConfig.MQTTConfig.proxy_to_client_enabled", + "ModuleConfig.MQTTConfig.root", + "ModuleConfig.MQTTConfig.tls_enabled", + "ModuleConfig.MQTTConfig.username", + "ModuleConfig.MapReportSettings.publish_interval_secs", + "ModuleConfig.MapReportSettings.should_report_location", + "ModuleConfig.MeshBeaconConfig.broadcast_interval_secs", + "ModuleConfig.MeshBeaconConfig.broadcast_message", + "ModuleConfig.NeighborInfoConfig.enabled", + "ModuleConfig.NeighborInfoConfig.transmit_over_lora", + "ModuleConfig.NeighborInfoConfig.update_interval", + "ModuleConfig.PaxcounterConfig.ble_threshold", + "ModuleConfig.PaxcounterConfig.enabled", + "ModuleConfig.PaxcounterConfig.paxcounter_update_interval", + "ModuleConfig.PaxcounterConfig.wifi_threshold", + "ModuleConfig.RangeTestConfig.enabled", + "ModuleConfig.RangeTestConfig.save", + "ModuleConfig.RangeTestConfig.sender", + "ModuleConfig.SerialConfig.baud", + "ModuleConfig.SerialConfig.echo", + "ModuleConfig.SerialConfig.enabled", + "ModuleConfig.SerialConfig.mode", + "ModuleConfig.SerialConfig.rxd", + "ModuleConfig.SerialConfig.timeout", + "ModuleConfig.SerialConfig.txd", + "ModuleConfig.StoreForwardConfig.enabled", + "ModuleConfig.StoreForwardConfig.heartbeat", + "ModuleConfig.StoreForwardConfig.history_return_max", + "ModuleConfig.StoreForwardConfig.history_return_window", + "ModuleConfig.StoreForwardConfig.is_server", + "ModuleConfig.StoreForwardConfig.records", + "ModuleConfig.TAKConfig.role", + "ModuleConfig.TAKConfig.team", + "ModuleConfig.TelemetryConfig.air_quality_enabled", + "ModuleConfig.TelemetryConfig.air_quality_interval", + "ModuleConfig.TelemetryConfig.device_telemetry_enabled", + "ModuleConfig.TelemetryConfig.device_update_interval", + "ModuleConfig.TelemetryConfig.environment_display_fahrenheit", + "ModuleConfig.TelemetryConfig.environment_measurement_enabled", + "ModuleConfig.TelemetryConfig.environment_screen_enabled", + "ModuleConfig.TelemetryConfig.environment_update_interval", + "ModuleConfig.TelemetryConfig.power_measurement_enabled", + "ModuleConfig.TelemetryConfig.power_screen_enabled", + "ModuleConfig.TelemetryConfig.power_update_interval", + "ModuleConfig.TrafficManagementConfig.nodeinfo_direct_response_max_hops", + "ModuleConfig.TrafficManagementConfig.position_min_interval_secs", + "ModuleConfig.TrafficManagementConfig.rate_limit_max_packets", + "ModuleConfig.TrafficManagementConfig.rate_limit_window_secs", + "ModuleConfig.TrafficManagementConfig.unknown_packet_threshold", + ) + + @Test + fun `every field named as not offered still exists and still carries a label`() { + val labelled = labelledFields() + val stale = notOffered.keys - labelled + + assertTrue( + stale.isEmpty(), + "these fields are no longer labelled by the schema, so they cannot still be listed as not offered: " + + stale.sorted().joinToString(), + ) + } + + @Test + fun `every field the schema labels has been accounted for`() { + val labelled = labelledFields() + val arrived = labelled - accountedFor + val departed = accountedFor - labelled + + assertEquals( + emptySet(), + arrived, + "the schema labels these fields and nobody has decided what this app does with them. Give each a control " + + "or a reason in notOffered, then add it to accountedFor: " + + arrived.sorted().joinToString(), + ) + assertEquals( + emptySet(), + departed, + "these fields no longer carry a label upstream, so drop them from accountedFor: " + + departed.sorted().joinToString(), + ) + } + + /** `Config.LoRaConfig.hop_limit` for every field of every config message the schema labels. */ + private fun labelledFields(): Set = configMessagePaths() + .flatMap { path -> + Class.forName(GENERATED_PACKAGE + path.replace('.', '$')).declaredFields.mapNotNull { property -> + val tag = property.getAnnotation(WireField::class.java)?.tag ?: return@mapNotNull null + val label = FieldMetadataRegistry.get(PROTO_PACKAGE + path, tag)?.label + "$path.${property.name}".takeIf { !label.isNullOrBlank() } + } + } + .toSet() + + /** Proto paths of the message types under `Config` and `ModuleConfig`, read from the protobufs jar's class list. */ + private fun configMessagePaths(): List { + val jar = File(FieldMetadataRegistry::class.java.protectionDomain.codeSource.location.toURI()) + val prefix = GENERATED_PACKAGE.replace('.', '/') + return ZipFile(jar).use { zip -> + zip.entries() + .asSequence() + .map { it.name } + .filter { it.startsWith(prefix) && it.endsWith(".class") && '/' !in it.removePrefix(prefix) } + .map { it.removePrefix(prefix).removeSuffix(".class").replace('$', '.') } + .filter { path -> + path.split('.').let { it.size > 1 && it.first() in CONTAINERS && it.all(::isTypeName) } + } + .filter { Class.forName(GENERATED_PACKAGE + it.replace('.', '$')).enumConstants == null } + .sorted() + .toList() + } + } + + private fun isTypeName(segment: String) = segment.isNotEmpty() && segment.first().isUpperCase() + + private companion object { + const val GENERATED_PACKAGE = "org.meshtastic.proto." + const val PROTO_PACKAGE = "meshtastic." + val CONTAINERS = setOf("Config", "ModuleConfig") + } +} diff --git a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt index a1b987339a..42c3acd03f 100644 --- a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt +++ b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.updateAll import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -35,6 +36,7 @@ private const val WIDGET_UPDATE_DEBOUNCE_MS = 500L @Single class AndroidAppWidgetUpdater(private val context: Context, stateProvider: LocalStatsWidgetStateProvider) : AppWidgetUpdater { + @Suppress("InjectDispatcher") // feature:widget does not depend on core:di, where CoroutineDispatchers lives private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) init { @@ -57,6 +59,8 @@ class AndroidAppWidgetUpdater(private val context: Context, stateProvider: Local @Suppress("TooGenericExceptionCaught") try { LocalStatsWidget().updateAll(context) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Logger.e(e) { "Failed to update widgets" } } diff --git a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidget.kt b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidget.kt index 1b22675ed0..a3a60d5885 100644 --- a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidget.kt +++ b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidget.kt @@ -67,8 +67,8 @@ import org.jetbrains.compose.resources.stringResource import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.meshtastic.core.common.util.DateFormatter +import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.air_utilization import org.meshtastic.core.resources.battery @@ -76,6 +76,7 @@ import org.meshtastic.core.resources.channel_utilization import org.meshtastic.core.resources.connecting import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected +import org.meshtastic.core.resources.formatDuration import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.local_stats_bad import org.meshtastic.core.resources.local_stats_diagnostics_prefix @@ -221,14 +222,14 @@ class LocalStatsWidget : Row(modifier = GlanceModifier.fillMaxWidth()) { StatRow( label = stringResource(Res.string.channel_utilization), - value = "%.1f%%".format(state.channelUtilization), + value = MetricFormatter.percent(state.channelUtilization), progress = state.channelUtilizationProgress, isSmall = isSmall, modifier = GlanceModifier.defaultWeight().padding(end = 4.dp), ) StatRow( label = stringResource(Res.string.air_utilization), - value = "%.1f%%".format(state.airUtilization), + value = MetricFormatter.percent(state.airUtilization), progress = state.airUtilizationProgress, isSmall = isSmall, modifier = GlanceModifier.defaultWeight().padding(start = 4.dp), @@ -368,7 +369,7 @@ class LocalStatsWidget : style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 10.sp), ) Text( - text = formatUptime(state.uptimeSecs.toInt()), + text = formatDuration(state.uptimeSecs), maxLines = 1, style = TextStyle( diff --git a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt index 3ca967119f..4905508fbb 100644 --- a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt +++ b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt @@ -79,6 +79,7 @@ data class LocalStatsWidgetUiState( @Single class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, connectionStateProvider: ConnectionStateProvider) { + @Suppress("InjectDispatcher") // feature:widget does not depend on core:di, where CoroutineDispatchers lives private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) diff --git a/feature/wifi-provision/detekt-baseline.xml b/feature/wifi-provision/detekt-baseline.xml index e772b78633..1ff1309a25 100644 --- a/feature/wifi-provision/detekt-baseline.xml +++ b/feature/wifi-provision/detekt-baseline.xml @@ -11,5 +11,6 @@ PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun NetworkRowPreview PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ProvisionStatusCardSuccessPreview PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ScanningBlePreview + UnnecessaryLaunchedEffect:WifiProvisionScreen.kt:LaunchedEffect diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModel.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModel.kt index 631434412a..cbeef6b5e6 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModel.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModel.kt @@ -19,14 +19,19 @@ package org.meshtastic.feature.wifiprovision import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.common.di.ApplicationCoroutineScope import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.feature.wifiprovision.domain.NymeaWifiService import org.meshtastic.feature.wifiprovision.model.ProvisionResult @@ -110,14 +115,17 @@ class WifiProvisionViewModel( private val bleScanner: BleScanner, private val bleConnectionFactory: BleConnectionFactory, private val dispatchers: CoroutineDispatchers, + private val applicationScope: ApplicationCoroutineScope, ) : ViewModel() { private val _uiState = MutableStateFlow(WifiProvisionUiState()) val uiState: StateFlow = _uiState.asStateFlow() - /** Lazily-created service; reset on [reset]. */ + /** Lazily-created service; replaced by [connectToDevice], closed by [disconnect] and [onCleared]. */ private var service: NymeaWifiService? = null + private var connectJob: Job? = null + // region Public actions (called from UI) /** @@ -130,9 +138,13 @@ class WifiProvisionViewModel( fun connectToDevice(address: String? = null) { _uiState.update { it.copy(phase = WifiProvisionUiState.Phase.ConnectingBle, error = null) } - viewModelScope.launch { - val nymeaService = NymeaWifiService(bleScanner, bleConnectionFactory, dispatchers.default) - service = nymeaService + val previousJob = connectJob + val previousService = service + val nymeaService = NymeaWifiService(bleScanner, bleConnectionFactory, dispatchers.default) + service = nymeaService + + connectJob = viewModelScope.launch { + release(previousJob, previousService) nymeaService .connect(address) @@ -211,9 +223,10 @@ class WifiProvisionViewModel( /** Disconnect and close any active BLE connection. */ fun disconnect() { + val nymeaService = service + service = null viewModelScope.launch { - service?.close() - service = null + nymeaService?.close() _uiState.value = WifiProvisionUiState() } } @@ -222,11 +235,26 @@ class WifiProvisionViewModel( override fun onCleared() { super.onCleared() - service?.cancel() + val job = connectJob + val nymeaService = service + service = null + // viewModelScope is already cancelled here, and the peripheral is only released by close(). + applicationScope.launch { release(job, nymeaService) } } // region Private helpers + /** + * Waits out [job] so a peripheral it was still installing belongs to [nymeaService] before that closes. Runs + * non-cancellable so a caller cancelled mid-release still closes the service. + */ + private suspend fun release(job: Job?, nymeaService: NymeaWifiService?) { + withContext(NonCancellable) { + job?.cancelAndJoin() + nymeaService?.close() + } + } + private suspend fun loadNetworks(nymeaService: NymeaWifiService) { _uiState.update { it.copy(phase = WifiProvisionUiState.Phase.LoadingNetworks) } diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiService.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiService.kt index eb1f50c7e4..1fcc696b04 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiService.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiService.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withTimeout -import kotlinx.serialization.encodeToString import org.meshtastic.core.ble.BleCharacteristic import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleConnectionState @@ -117,7 +116,7 @@ class NymeaWifiService( .onEach { bytes -> val message = reassembler.feed(bytes) if (message != null) { - Logger.d { "$TAG: ← $message" } + Logger.d { "$TAG: ← response (${message.length} chars)" } responseChannel.trySend(message) } if (!subscribed.isCompleted) subscribed.complete(Unit) @@ -145,14 +144,14 @@ class NymeaWifiService( */ suspend fun scanNetworks(): Result> = safeCatching { // Trigger scan - sendCommand(NymeaJson.encodeToString(NymeaSimpleCommand(CMD_SCAN))) + sendCommand(CMD_SCAN, NymeaJson.encodeToString(NymeaSimpleCommand(CMD_SCAN))) val scanAck = NymeaJson.decodeFromString(waitForResponse()) if (scanAck.responseCode != RESPONSE_SUCCESS) { error("Scan command failed: ${nymeaErrorMessage(scanAck.responseCode)}") } // Fetch results - sendCommand(NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_NETWORKS))) + sendCommand(CMD_GET_NETWORKS, NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_NETWORKS))) val networksResponse = NymeaJson.decodeFromString(waitForResponse()) if (networksResponse.responseCode != RESPONSE_SUCCESS) { error("GetNetworks failed: ${nymeaErrorMessage(networksResponse.responseCode)}") @@ -186,7 +185,7 @@ class NymeaWifiService( ) return safeCatching { - sendCommand(json) + sendCommand(cmd, json) val response = NymeaJson.decodeFromString(waitForResponse()) if (response.responseCode == RESPONSE_SUCCESS) { val ipAddress = @@ -202,31 +201,27 @@ class NymeaWifiService( } } - /** Disconnect and cancel the service scope. */ + /** Disconnect, which releases the BLE peripheral, then cancel the service scope. */ suspend fun close() { - bleConnection.disconnect() - reassembler.reset() - serviceScope.cancel() - } - - /** - * Synchronous teardown — cancels the service scope (and its child BLE connection) without suspending. - * - * Use this from `ViewModel.onCleared()` where `viewModelScope` is already cancelled and launching a new coroutine - * is not possible. - */ - fun cancel() { - reassembler.reset() - serviceScope.cancel() + try { + bleConnection.disconnect() + } finally { + reassembler.reset() + serviceScope.cancel() + } } // endregion // region Internal helpers - /** Encode [json] into ≤20-byte packets and write each one WITH_RESPONSE to the commander characteristic. */ - private suspend fun sendCommand(json: String) { - Logger.d { "$TAG: → $json" } + /** + * Encode [json] into ≤20-byte packets and write each one WITH_RESPONSE to the commander characteristic. Only the + * [command] code is logged: a Connect payload carries the WiFi password, and even its length gives away the SSID + * and password lengths. + */ + private suspend fun sendCommand(command: Int, json: String) { + Logger.d { "$TAG: → command=$command" } val packets = NymeaPacketCodec.encode(json) bleConnection.profile(WIRELESS_SERVICE_UUID) { service -> for (packet in packets) { @@ -245,9 +240,8 @@ class NymeaWifiService( * Uses a short timeout because this is an optional enrichment for UX, not a provisioning success criterion. */ private suspend fun fetchConnectionIpAddress(): String? = safeCatching { - sendCommand(NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_CONNECTION))) - val response = - NymeaJson.decodeFromString(waitForResponse(timeout = CONNECTION_INFO_TIMEOUT)) + sendCommand(CMD_GET_CONNECTION, NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_CONNECTION))) + val response = NymeaJson.decodeFromString(waitForResponse(timeout = CONNECTION_INFO_TIMEOUT)) if (response.responseCode == RESPONSE_SUCCESS) { response.connectionInfo?.ipAddress?.takeIf { it.isNotBlank() } } else { diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt index d3d4afe06f..024d6d0627 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -70,6 +71,7 @@ internal fun ProvisionStatusCard(provisionStatus: ProvisionStatus, isProvisionin /** Resolve container + content color pair for the provision status card. */ @Composable +@ReadOnlyComposable private fun statusCardColors(provisionStatus: ProvisionStatus, isProvisioning: Boolean): Pair = when { isProvisioning -> MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.onSecondaryContainer diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt index e5442f3567..c501ed0a0f 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt @@ -183,7 +183,7 @@ fun WifiProvisionScreen( } LaunchedEffect(uiState.error) { errorMessage?.let { snackbarHostState.showSnackbar(it) } } - LaunchedEffect(Unit) { viewModel.connectToDevice(address) } + LaunchedEffect(viewModel, address) { viewModel.connectToDevice(address) } Scaffold( topBar = { diff --git a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/DeduplicateBySsidTest.kt b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/DeduplicateBySsidTest.kt index 2ad2e1fcc6..22f87b9ff5 100644 --- a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/DeduplicateBySsidTest.kt +++ b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/DeduplicateBySsidTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.wifiprovision import org.meshtastic.feature.wifiprovision.model.WifiNetwork diff --git a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModelTest.kt b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModelTest.kt index eb658c0ff0..d8f480aa27 100644 --- a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModelTest.kt +++ b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModelTest.kt @@ -14,18 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.wifiprovision +import androidx.lifecycle.ViewModelStore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.meshtastic.core.ble.BleConnection +import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.testing.FakeApplicationCoroutineScope import org.meshtastic.core.testing.FakeBleConnection import org.meshtastic.core.testing.FakeBleConnectionFactory import org.meshtastic.core.testing.FakeBleDevice @@ -62,18 +66,16 @@ class WifiProvisionViewModelTest { Dispatchers.setMain(testDispatcher) scanner = FakeBleScanner() connection = FakeBleConnection() - viewModel = - WifiProvisionViewModel( - bleScanner = scanner, - bleConnectionFactory = FakeBleConnectionFactory(connection), - dispatchers = CoroutineDispatchers( - io = testDispatcher, - main = testDispatcher, - default = testDispatcher, - ), - ) + viewModel = createViewModel(FakeBleConnectionFactory(connection)) } + private fun createViewModel(connectionFactory: BleConnectionFactory) = WifiProvisionViewModel( + bleScanner = scanner, + bleConnectionFactory = connectionFactory, + dispatchers = CoroutineDispatchers(io = testDispatcher, main = testDispatcher, default = testDispatcher), + applicationScope = FakeApplicationCoroutineScope(testDispatcher), + ) + @AfterTest fun tearDown() { Dispatchers.resetMain() @@ -323,10 +325,72 @@ class WifiProvisionViewModelTest { assertTrue(connection.disconnectCalls >= 1, "BLE disconnect should be called") } + // ----------------------------------------------------------------------- + // Connection ownership + // ----------------------------------------------------------------------- + + @Test + fun `reconnecting closes the previous BLE connection`() = runTest { + val factory = RecordingBleConnectionFactory() + val viewModel = createViewModel(factory) + scanner.emitDevice(FakeBleDevice("AA:BB:CC:DD:EE:FF")) + + viewModel.connectToDevice() + advanceUntilIdle() + viewModel.connectToDevice() + advanceUntilIdle() + + assertEquals(2, factory.created.size) + assertEquals(1, factory.created[0].disconnectCalls) + assertEquals(0, factory.created[1].disconnectCalls) + assertEquals(Phase.DeviceFound, viewModel.uiState.value.phase) + } + + @Test + fun `reconnecting cancels a connect that is still scanning`() = runTest { + val factory = RecordingBleConnectionFactory() + val viewModel = createViewModel(factory) + + viewModel.connectToDevice() + runCurrent() + viewModel.connectToDevice() + runCurrent() + scanner.emitDevice(FakeBleDevice("AA:BB:CC:DD:EE:FF")) + advanceUntilIdle() + + assertEquals(0, factory.created[0].connectAndAwaitCalls) + assertEquals(1, factory.created[1].connectAndAwaitCalls) + assertEquals(Phase.DeviceFound, viewModel.uiState.value.phase) + } + + @Test + fun `clearing the ViewModel closes the current BLE connection`() = runTest { + val factory = RecordingBleConnectionFactory() + val viewModel = createViewModel(factory) + scanner.emitDevice(FakeBleDevice("AA:BB:CC:DD:EE:FF")) + viewModel.connectToDevice() + advanceUntilIdle() + + val store = ViewModelStore() + store.put("wifiProvisionViewModel", viewModel) + store.clear() + advanceUntilIdle() + + assertEquals(1, factory.created.single().disconnectCalls) + } + // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- + /** Hands out a fresh [FakeBleConnection] per [create], so each service's connection can be checked on its own. */ + private class RecordingBleConnectionFactory : BleConnectionFactory { + val created = mutableListOf() + + override fun create(scope: CoroutineScope, tag: String): BleConnection = + FakeBleConnection().also { created += it } + } + /** * Emit a complete nymea JSON response on the Commander Response characteristic. Uses newline-terminated encoding * matching [NymeaPacketCodec]. diff --git a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaPacketCodecTest.kt b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaPacketCodecTest.kt index e743fcb9be..d65311ccd0 100644 --- a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaPacketCodecTest.kt +++ b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaPacketCodecTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.wifiprovision.domain import kotlin.test.Test diff --git a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaProtocolTest.kt b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaProtocolTest.kt index 8f2151fb80..2210783ea9 100644 --- a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaProtocolTest.kt +++ b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaProtocolTest.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.wifiprovision.domain import kotlinx.serialization.encodeToString diff --git a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiServiceTest.kt b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiServiceTest.kt index e356daa267..0a5378857b 100644 --- a/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiServiceTest.kt +++ b/feature/wifi-provision/src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiServiceTest.kt @@ -14,14 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("MagicNumber") - package org.meshtastic.feature.wifiprovision.domain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.meshtastic.core.ble.BleWriteType +import org.meshtastic.core.testing.CapturingLogWriter import org.meshtastic.core.testing.FakeBleConnection import org.meshtastic.core.testing.FakeBleConnectionFactory import org.meshtastic.core.testing.FakeBleDevice @@ -29,6 +28,7 @@ import org.meshtastic.core.testing.FakeBleScanner import org.meshtastic.feature.wifiprovision.NymeaBleConstants.COMMANDER_RESPONSE_UUID import org.meshtastic.feature.wifiprovision.NymeaBleConstants.WIRELESS_COMMANDER_UUID import org.meshtastic.feature.wifiprovision.model.ProvisionResult +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -43,6 +43,11 @@ class NymeaWifiServiceTest { private val address = "AA:BB:CC:DD:EE:FF" + @AfterTest + fun tearDown() { + CapturingLogWriter.uninstall() + } + private fun createService( scanner: FakeBleScanner = FakeBleScanner(), connection: FakeBleConnection = FakeBleConnection(), @@ -293,6 +298,24 @@ class NymeaWifiServiceTest { assertTrue(writes.contains("\"c\":2"), "Should send CMD_CONNECT_HIDDEN (2)") } + @Test + fun `provision never logs the WiFi credentials or the device response payload`() = runTest { + val connection = FakeBleConnection() + val (service, scanner) = createService(connection = connection) + connectService(service, scanner) + val logs = CapturingLogWriter.install() + + emitResponse(connection, """{"c":1,"r":0,"p":{"i":"10.77.88.99"}}""") + val result = service.provision("SecretHomeNet", "hunter2-wifi-pass") + + assertIs(result) + assertTrue( + logs.messages().any { it.endsWith("command=1") }, + "Command send should be logged with nothing after the code: ${logs.messages()}", + ) + logs.assertNotLogged("hunter2-wifi-pass", "SecretHomeNet", "10.77.88.99") + } + @Test fun `provision returns Failure on exception`() = runTest { // Create a service with a connection that will fail writes after connecting diff --git a/gradle.properties b/gradle.properties index 0c0d58f7e6..dfe4f8d6b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,4 @@ -# --- Android build features --- -android.defaults.buildfeatures.resvalues=false -android.defaults.buildfeatures.shaders=false +# --- Android --- # Lint analysis runs in its own worker, so neither org.gradle.jvmargs nor # kotlin.daemon.jvmargs applies to it. The default heap OOMs on a cold analysis. android.experimental.lint.heapSize=3G @@ -16,10 +14,11 @@ kotlin.code.style=official # Only kotlin.daemon.jvmargs is read from here; kotlin.daemon.jvm.options is a system # property. Unset, the daemon inherits org.gradle.jvmargs' 8g and over-commits CI runners. kotlin.daemon.jvmargs=-Xmx6g -XX\:+UseG1GC -XX\:SoftRefLRUPolicyMSPerMB=1 -XX\:ReservedCodeCacheSize=320m -XX\:+HeapDumpOnOutOfMemoryError -kotlin.daemon.useFallback=false -# The Kotlin/Native compiler runs in its own JVM, governed by neither of the heaps -# above. Its 3g default OOMs compiling every module for iOS from a cold cache. +# Applies only where kotlin.native.disableCompilerDaemon forks the Kotlin/Native compiler, as CI +# does; otherwise it runs inside the Gradle daemon and shares org.gradle.jvmargs' heap. kotlin.native.jvmArgs=-Xmx5g -XX\:+UseG1GC -XX\:+HeapDumpOnOutOfMemoryError +# Only macOS can run iosSimulatorArm64Test; this hides KGP's notice about it on Linux and Windows hosts. +kotlin.native.ignoreDisabledTargets=true # --- KSP --- # (incremental processing is default-on in KSP2; only the non-default flag is declared) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28e84230fc..3a5205c8bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,11 +4,11 @@ xmlutil = "1.0.2.1" # Android agp = "9.4.1" appcompat = "1.8.0" -appfunctions = "1.0.0-alpha11" +appfunctions = "1.0.0-alpha12" # androidx androidx-sqlite-bundled = "2.7.1" -androidx-work = "2.11.2" +androidx-work = "2.12.0" datastore = "1.2.1" glance = "1.2.0" lifecycle = "2.11.0" @@ -17,7 +17,7 @@ navigation3 = "1.2.0-beta01" # navigation3-runtime is AndroidX's own KMP artifact, separate from the JetBrains # navigation3-ui mirror above; the two refs move independently and the mirror does not # publish every AndroidX version, so they are not always equal. -navigation3-runtime = "1.2.0-rc01" +navigation3-runtime = "1.2.0" navigationevent = "1.1.0" paging = "3.5.1" room = "3.0.3" @@ -29,12 +29,12 @@ kotlin = "2.4.20" kotlinx-coroutines-android = "1.11.0" kotlinx-datetime = "0.8.0-0.6.x-compat" kotlinx-serialization = "1.11.0" -ktlint = "1.7.1" -ktfmt = "0.61" -kover = "0.9.9" +ktlint = "1.8.0" +ktfmt = "0.64" +kover = "0.9.11" mokkery = "3.5.0" junit5 = "6.1.3" -junit-platform = "6.1.3" # aligned with junit5 — JUnit Platform uses 1.x scheme +junit-platform = "6.1.3" # JUnit Platform shares the junit5 version since JUnit 6 kotest = "6.2.5" konsist = "0.17.3" turbine = "1.2.1" @@ -44,14 +44,13 @@ compose-screenshot = "0.0.1-alpha16" # Baseline Profiles / Macrobenchmark # `benchmark` drives both the androidx.benchmark macro lib AND the androidx.baselineprofile -# Gradle plugin (same coordinates/version). Kept on the alpha track to stay compatible with -# AGP 9.x (the stable 1.4.x line predates AGP 9 support). +# Gradle plugin (same coordinates/version). The stable 1.4.x line predates AGP 9 support. benchmark = "1.5.0" profileinstaller = "1.4.1" androidx-uiautomator = "2.4.0" # Compose Multiplatform -compose-multiplatform = "1.12.0" +compose-multiplatform = "1.12.1" compose-multiplatform-material3 = "1.12.0-alpha03" # `androidx-compose-bom-aligned` tracks androidx.compose.{runtime,ui,foundation,animation} # artifacts that ship in lockstep with CMP. Kept as a separate version ref so Renovate @@ -70,9 +69,9 @@ jetbrains-adaptive = "1.3.0-rc01" # won conflict resolution, so every KML/KMZ import died with NoSuchMethodError (fatal in prod, # 2.8.0–2.8.1). KML is now read by the app's own xmlutil-based converter and that parser is unused, so # the pairing — and the canary test that guarded it — is gone. The floor stays as a plain minimum. -android-maps-utils = "5.2.0" -maplibre-compose = "0.17.0" -maps-compose = "8.6.0" +android-maps-utils = "6.0.0" +maplibre-compose = "0.19.0" +maps-compose = "9.0.0" # ML Kit mlkit-barcode-scanning = "17.3.0" @@ -91,32 +90,31 @@ aboutlibraries = "15.2.0" jserialcomm = "2.11.4" coil = "3.6.3" datadog-gradle = "1.31.0" -dd-sdk-android = "3.14.0" +dd-sdk-android = "3.14.1" detekt = "2.0.0-alpha.6" dokka = "2.2.0" devtools-ksp = "2.3.12" firebase-crashlytics-gradle = "3.0.8" google-services-gradle = "4.5.0" -markdown = "0.7.14" +markdown = "0.7.16" markdownRenderer = "0.45.0" okio = "3.18.2" uri-kmp = "0.0.21" -spotless = "8.10.2" +spotless = "8.10.3" vico = "3.3.1" kable = "0.45.0" -mqttastic = "0.8.2" +mqttastic = "0.9.0" jmdns = "3.6.3" qrcode-kotlin = "4.5.0" takpacket-sdk = "0.9.2-SNAPSHOT" # PoC: DisplayFrame screen mirroring, locally published from protobufs@screen-mirror-poc. -# Revert to main's pin once protobufs#1054 is tagged. main tracks 2.8.0.96-g16fbc6c-SNAPSHOT. +# Revert to main's pin once protobufs#1054 is tagged. main pins 2.8.1. # (requires -PuseMavenLocal or the gradle.properties flag on this branch) meshtastic-protobufs = "2.99.0-screen-mirror-poc-SNAPSHOT" # Gradle Plugins ccud = "2.8.0" -develocity = "4.5.1" -foojay-resolver = "1.0.0" +develocity = "4.6.0" [libraries] @@ -125,7 +123,7 @@ xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", v # AndroidX androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.13.0" } -androidx-annotation = { module = "androidx.annotation:annotation", version = "1.10.0" } +androidx-annotation = { module = "androidx.annotation:annotation", version = "1.11.0" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" } androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctions" } @@ -135,7 +133,7 @@ androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", versi androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" } androidx-camera-viewfinder-compose = { module = "androidx.camera.viewfinder:viewfinder-compose", version = "1.6.2" } -androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.19.0" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.19.1" } androidx-core-location-altitude = { module = "androidx.core:core-location-altitude", version = "1.0.0" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" } androidx-security-crypto = { module = "androidx.security:security-crypto", version = "1.1.0" } @@ -340,7 +338,7 @@ compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle- compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" } datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version.ref = "datadog-gradle" } compose-screenshot-gradlePlugin = { module = "com.android.compose.screenshot:screenshot-test-gradle-plugin", version.ref = "compose-screenshot" } -detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.6.6" } +detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.6.7" } detekt-formatting = { module = "dev.detekt:detekt-rules-ktlint-wrapper", version.ref = "detekt" } detekt-gradlePlugin = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" } firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" } @@ -384,7 +382,6 @@ dd-sdk-android = [ # Android android-application = { id = "com.android.application", version.ref = "agp" } android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } -android-test = { id = "com.android.test" } androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" } compose-screenshot = { id = "com.android.compose.screenshot", version.ref = "compose-screenshot" } @@ -413,9 +410,6 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } room = { id = "androidx.room3", version.ref = "room" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } -develocity = { id = "com.gradle.develocity", version.ref = "develocity" } -foojay-resolver = { id = "org.gradle.toolchains.foojay-resolver", version.ref = "foojay-resolver" } - # Meshtastic meshtastic-android-application = { id = "meshtastic.android.application" } meshtastic-android-application-compose = { id = "meshtastic.android.application.compose" } @@ -427,6 +421,7 @@ meshtastic-android-room = { id = "meshtastic.android.room" } meshtastic-android-screenshot = { id = "meshtastic.android.screenshot" } # Vendored google/secrets-gradle-plugin (Isolated-Projects-safe) — see build-logic/convention. meshtastic-android-secrets = { id = "meshtastic.android.secrets" } +meshtastic-android-test = { id = "meshtastic.android.test" } meshtastic-detekt = { id = "meshtastic.detekt" } meshtastic-docs = { id = "meshtastic.docs" } meshtastic-koin = { id = "meshtastic.koin" } @@ -438,5 +433,4 @@ meshtastic-kmp-library = { id = "meshtastic.kmp.library" } meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" } meshtastic-root = { id = "meshtastic.root" } meshtastic-spotless = { id = "meshtastic.spotless" } -meshtastic-publishing = { id = "meshtastic.publishing" } meshtastic-aboutlibraries = { id = "meshtastic.aboutlibraries" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4cdef06fed..47158abb3f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,13 +2,13 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # -bin: nothing here reads -all's docs/sources. Update sha256 with the URL on upgrades # (official .sha256 sits next to the distribution; renovate does both). -# On 9.7.1 again after the #6786 pin-back. Both of that revert's blockers must stay dead -# on any future 9.7+ bump: the CC fingerprint crashing on reload of its own same-key entry -# (IsInIdeaSyncValueSource CNFE in the settings-plugin scope), and CMP's Windows packaging -# tripping an Isolated Projects violation (':desktopApp' cannot access Project.layout on -# ':'). The first only shows on a CC cache-key HIT, so one green run does not clear it. -distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +# Every bump must keep two failures dead: the CC fingerprint crashing on reload of its own +# same-key entry (IsInIdeaSyncValueSource CNFE in the settings-plugin scope), and CMP's Windows +# packaging tripping an Isolated Projects violation (':desktopApp' cannot access Project.layout +# on ':'). The first shows only on a configuration-cache hit, and CI keeps no configuration +# cache between runs, so check it locally by running the same build twice. +distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip networkTimeout=30000 retries=3 retryBackOffMs=500 diff --git a/marketing-screenshots/build.gradle.kts b/marketing-screenshots/build.gradle.kts deleted file mode 100644 index 88a23a7135..0000000000 --- a/marketing-screenshots/build.gradle.kts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.meshtastic.buildlogic.maplibreDesktopRuntime - -// Marketing (store listing) screenshots — GENERATE-ONLY, intentionally NOT run in CI. -// -// Unlike :screenshot-tests (a visual-regression gate) and :docs-screenshots (doc-framed compositions), this module -// is a plain JVM program that draws the Play Store / F-Droid listing screenshots: the app's own commonMain screens -// rendered offscreen with Compose Desktop's ImageComposeScene over one sample mesh, and the real MapLibre map captured -// through maplibre-compose's MapSnapshotter. Every shot is the raw screen, as Play requires ("do not position the -// screenshots within device frames"), at one size per form factor: phone 1080x1920, 7-inch 1080x1920, 10-inch -// 2560x1440, Chromebook 1920x1080, Android XR 1920x1200, and the desktop app's Flathub listing at 1280x800. The same -// screens adapt themselves - the app's navigation suite, list-detail and two-pane layouts do the work - so a form -// factor is data, not a copy of the loop. It has no tests, so `./gradlew test` / `allTests` never touch it; the one -// command is -// -// ./gradlew :marketing-screenshots:updateMarketingScreenshots -// -// which writes en-US straight into fastlane/metadata/android/en-US/images/Screenshots/ (phone, sevenInch, -// tenInch, chromebook, xr) and desktopApp/packaging/linux/screenshots/. Any other locale in -PmarketingLocales (comma -// list, default "en-US") renders into build/marketing-screenshots// in the same layout, for a later `fastlane -// supply` run: only en-US is committed because everything under fastlane/ is read straight from git by F-Droid and -// IzzyOnDroid. The sample prose lives in src/main/composeResources/values/strings.xml, which Crowdin's first rule -// already picks up. -// -PmarketingFramed=true also writes the framed 1242x2484 phone variants (bezel, caption banner) under -// build/marketing-screenshots/framed// for website and social use. The map needs a Vulkan loader the JVM can -// find: on a stock Ubuntu nothing, on a Nix host, whose shell replaces LD_LIBRARY_PATH with its own, pass -// -PmarketingLibraryPath=/usr/lib/x86_64-linux-gnu. -plugins { - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.compose.compiler) - alias(libs.plugins.compose.multiplatform) - alias(libs.plugins.meshtastic.detekt) - alias(libs.plugins.meshtastic.spotless) -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(25)) - vendor.set(JvmVendorSpec.JETBRAINS) - } - compilerOptions { jvmTarget.set(JvmTarget.JVM_25) } -} - -dependencies { - implementation(compose.desktop.currentOs) - implementation(libs.compose.multiplatform.foundation) - implementation(libs.compose.multiplatform.material3) - implementation(libs.compose.multiplatform.runtime) - implementation(libs.compose.multiplatform.ui) - implementation(libs.compose.multiplatform.resources) - implementation(libs.jetbrains.compose.material3.adaptive) - implementation(libs.jetbrains.compose.material3.adaptive.layout) - implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) - implementation(libs.maplibre.compose) - maplibreDesktopRuntime() - - implementation(projects.core.ble) - implementation(projects.core.common) - implementation(projects.core.database) - implementation(projects.core.model) - implementation(projects.core.navigation) - implementation(projects.core.resources) - implementation(projects.core.ui) - implementation(projects.feature.connections) - implementation(projects.feature.map) - implementation(projects.feature.mapMaplibre) - implementation(projects.feature.messaging) - implementation(projects.feature.node) - implementation(projects.feature.settings) - implementation(libs.meshtastic.protobufs) -} - -compose.resources { - publicResClass = false - packageOfResClass = "org.meshtastic.screenshot.marketing.resources" -} - -// Script-level vals are re-bound to locals inside the task block: a lambda that reads them directly captures the -// script object, which the configuration cache refuses to serialize. -val repositoryRoot = isolated.rootProject.projectDirectory -val marketingOutput = layout.buildDirectory.dir("marketing-screenshots") -val marketingLocales = providers.gradleProperty("marketingLocales").orElse("en-US") -val marketingFramed = providers.gradleProperty("marketingFramed").orElse("false") -// The property wins because a Nix dev shell rewrites LD_LIBRARY_PATH on entry, so the exported one never arrives. -val hostLibraryPath = - providers.gradleProperty("marketingLibraryPath").orElse(providers.environmentVariable("LD_LIBRARY_PATH")) - -tasks.register("updateMarketingScreenshots") { - description = - "Renders the store-listing screenshots into fastlane/metadata/android/en-US/images/ and " + - "desktopApp/packaging/linux/screenshots/ (other locales into build/marketing-screenshots/)." - group = "store-screenshots" - val output = repositoryRoot - val buildOutput = marketingOutput - val locales = marketingLocales - val framed = marketingFramed - val libraryPath = hostLibraryPath - mainClass.set("org.meshtastic.screenshot.marketing.MainKt") - classpath = sourceSets.main.get().runtimeClasspath - // maplibre-compose reaches maplibre-native through the FFM API, which refuses to load without this. - jvmArgs("--enable-native-access=ALL-UNNAMED", "-Djava.awt.headless=true", "-Xmx2G") - argumentProviders.add( - CommandLineArgumentProvider { - listOf(output.asFile.absolutePath, buildOutput.get().asFile.absolutePath, locales.get(), framed.get()) - }, - ) - // The generator must provably run without a display, whatever the daemon inherited; the Vulkan loader search - // path, on the other hand, is the caller's to set. - environment.remove("DISPLAY") - environment.remove("WAYLAND_DISPLAY") - libraryPath.orNull?.let { environment("LD_LIBRARY_PATH", it) } - outputs.upToDateWhen { false } -} diff --git a/marketing-screenshots/src/main/composeResources/values-ar/strings.xml b/marketing-screenshots/src/main/composeResources/values-ar/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ar/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-be/strings.xml b/marketing-screenshots/src/main/composeResources/values-be/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-be/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-bg/strings.xml b/marketing-screenshots/src/main/composeResources/values-bg/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-bg/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ca/strings.xml b/marketing-screenshots/src/main/composeResources/values-ca/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ca/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-cs/strings.xml b/marketing-screenshots/src/main/composeResources/values-cs/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-cs/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-de/strings.xml b/marketing-screenshots/src/main/composeResources/values-de/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-de/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-el/strings.xml b/marketing-screenshots/src/main/composeResources/values-el/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-el/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-es/strings.xml b/marketing-screenshots/src/main/composeResources/values-es/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-es/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-et/strings.xml b/marketing-screenshots/src/main/composeResources/values-et/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-et/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-fi/strings.xml b/marketing-screenshots/src/main/composeResources/values-fi/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-fi/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-fr/strings.xml b/marketing-screenshots/src/main/composeResources/values-fr/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-fr/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ga/strings.xml b/marketing-screenshots/src/main/composeResources/values-ga/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ga/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-gl/strings.xml b/marketing-screenshots/src/main/composeResources/values-gl/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-gl/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-he/strings.xml b/marketing-screenshots/src/main/composeResources/values-he/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-he/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-hr/strings.xml b/marketing-screenshots/src/main/composeResources/values-hr/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-hr/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ht/strings.xml b/marketing-screenshots/src/main/composeResources/values-ht/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ht/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-hu/strings.xml b/marketing-screenshots/src/main/composeResources/values-hu/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-hu/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-is/strings.xml b/marketing-screenshots/src/main/composeResources/values-is/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-is/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-it/strings.xml b/marketing-screenshots/src/main/composeResources/values-it/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-it/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ja/strings.xml b/marketing-screenshots/src/main/composeResources/values-ja/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ja/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ko/strings.xml b/marketing-screenshots/src/main/composeResources/values-ko/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ko/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-lt/strings.xml b/marketing-screenshots/src/main/composeResources/values-lt/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-lt/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-nl/strings.xml b/marketing-screenshots/src/main/composeResources/values-nl/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-nl/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-no/strings.xml b/marketing-screenshots/src/main/composeResources/values-no/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-no/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-pl/strings.xml b/marketing-screenshots/src/main/composeResources/values-pl/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-pl/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml b/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-pt/strings.xml b/marketing-screenshots/src/main/composeResources/values-pt/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-pt/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ro/strings.xml b/marketing-screenshots/src/main/composeResources/values-ro/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ro/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-ru/strings.xml b/marketing-screenshots/src/main/composeResources/values-ru/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-ru/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-sk/strings.xml b/marketing-screenshots/src/main/composeResources/values-sk/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-sk/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-sl/strings.xml b/marketing-screenshots/src/main/composeResources/values-sl/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-sl/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-sq/strings.xml b/marketing-screenshots/src/main/composeResources/values-sq/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-sq/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-sr/strings.xml b/marketing-screenshots/src/main/composeResources/values-sr/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-sr/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-srp/strings.xml b/marketing-screenshots/src/main/composeResources/values-srp/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-srp/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-sv/strings.xml b/marketing-screenshots/src/main/composeResources/values-sv/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-sv/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-tr/strings.xml b/marketing-screenshots/src/main/composeResources/values-tr/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-tr/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-uk/strings.xml b/marketing-screenshots/src/main/composeResources/values-uk/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-uk/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml b/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml b/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml deleted file mode 100644 index 68657761d4..0000000000 --- a/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/marketing-screenshots/src/main/composeResources/values/strings.xml b/marketing-screenshots/src/main/composeResources/values/strings.xml deleted file mode 100644 index 990845c8ae..0000000000 --- a/marketing-screenshots/src/main/composeResources/values/strings.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - Set up a private encrypted mesh with your group in seconds. - Share a channel with a QR code - Positions shared over the mesh, with no internet required. - Every node on the map - Messages hop node to node over LoRa. No towers, no SIM, no subscription. - Text without cell service - Signal, uptime, hops and security, one tap from the list. - Every node in detail - Battery, signal, distance and hops for every node you can hear. - See everyone on your mesh - OpenFreeMap © OpenMapTiles Data from OpenStreetMap - Water at the crossing is low, safe to ford - Heading up from the trailhead now, 4 of us - Lunch at the lookout, back on the air in 30 - Made the ridge, good signal back to base - Parked at the overflow lot, radio on - Great, we'll take the crossing route back - Bringing the truck around to the lower lot at 3 - Copy. Weather window closes around 2, keep moving - Can you grab the first aid kit from the truck? - Can you see me on the map yet? - Got the spare battery in the truck if anyone needs it - Trail crew meets at the north gate at 8 - Thanks for the weather heads-up - %1$s (%2$d) - diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt deleted file mode 100644 index e83ed4eea2..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.ble.BleConnectionState -import org.meshtastic.core.ble.BleDevice -import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.DeviceType -import org.meshtastic.core.model.Node -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.connections -import org.meshtastic.core.resources.disconnect -import org.meshtastic.core.resources.firmware_version -import org.meshtastic.core.resources.rssi -import org.meshtastic.core.resources.unknown -import org.meshtastic.core.ui.component.AdaptiveTwoPane -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.connections.model.DeviceListEntry -import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedInfo -import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedText -import org.meshtastic.feature.connections.ui.components.DeviceList -import org.meshtastic.feature.connections.ui.components.TransportSelector - -/** - * The connections tab as the app lays it out: the connected radio's card and the transport selector first, the - * discovery list second, side by side in the same [AdaptiveTwoPane] on an expanded window. Base Camp is connected over - * Bluetooth, with two more of the group's radios in range; the list shows one transport at a time, the app's rule. - */ -@Composable -internal fun ConnectionsScreen(mesh: SampleMesh) { - val radios = mesh.bleRadios() - MarketingTheme { - AppShell(TopLevelDestination.Connect) { - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.connections), - ourNode = mesh.baseCamp, - showNodeChip = true, - canNavigateUp = false, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - Column( - modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Spacer(modifier = Modifier.height(4.dp)) - AdaptiveTwoPane( - first = { - ConnectedCard(mesh.baseCamp) - TransportSelector(activeTransport = DeviceType.BLE, onSelectTransport = {}) - }, - second = { - Box(modifier = Modifier.weight(1f).fillMaxWidth()) { - DeviceList( - connectionState = ConnectionState.Connected, - selectedDevice = radios.first().fullAddress, - bleDevices = radios, - usbDevices = emptyList(), - discoveredTcpDevices = emptyList(), - recentTcpDevices = emptyList(), - isBleScanning = false, - isNetworkScanning = false, - activeTransport = DeviceType.BLE, - onSelectDevice = {}, - onToggleBleScan = {}, - onToggleNetworkScan = {}, - onAddManualAddress = { _, _ -> }, - onRemoveRecentAddress = {}, - ) - } - }, - ) - } - } - } - } -} - -/** The connected-device card in the app's connected state: the same card shell and the feature's own contents. */ -@Composable -private fun ConnectedCard(node: Node) { - Card(modifier = Modifier.fillMaxWidth()) { - Box(modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp), contentAlignment = Alignment.Center) { - CurrentlyConnectedInfo( - node = node, - text = - CurrentlyConnectedText( - unknownLabel = stringResource(Res.string.unknown), - rssiLabel = stringResource(Res.string.rssi), - disconnectLabel = stringResource(Res.string.disconnect), - firmwareVersion = stringResource(Res.string.firmware_version, SampleMesh.FIRMWARE_VERSION), - ), - onNavigateToNodeDetails = {}, - onClickDisconnect = {}, - ) - } - } -} - -/** - * The Bluetooth pane's radios: Base Camp, bonded and connected, so its row carries the node chip the app shows for a - * radio it has met before; then two of the group's radios advertising nearby under their factory names, which are the - * node number's last four hex digits. - */ -private fun SampleMesh.bleRadios(): List = listOf( - DeviceListEntry.Ble(SampleBleDevice(baseCamp, rssi = -58, connected = true), node = baseCamp), - DeviceListEntry.Ble(SampleBleDevice(trailhead, rssi = -71, connected = false)), - DeviceListEntry.Ble(SampleBleDevice(sarahsTruck, rssi = -84, connected = false)), -) - -/** A radio as the scanner would report it: advertised name and address derived from the node it belongs to. */ -private class SampleBleDevice(node: Node, override val rssi: Int, connected: Boolean) : BleDevice { - private val hex = node.num.toUInt().toString(16).padStart(8, '0') - - override val name: String = "Meshtastic_${hex.takeLast(4)}" - - override val address: String = hex.takeLast(6).uppercase().chunked(2).joinToString(":", prefix = "F0:9E:9E:") - - override val state: StateFlow = - MutableStateFlow(if (connected) BleConnectionState.Connected else BleConnectionState.Disconnected()) - - override val isBonded: Boolean = true - - override val isConnected: Boolean = connected - - override suspend fun readRssi(): Int = rssi - - override suspend fun bond() = Unit -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt deleted file mode 100644 index 76f187da59..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -/** - * One listing surface: the pixel size the store wants, the density that puts the app's window class where that device's - * users see it, which shots it lists and where they go. The screens are surface-agnostic; only this changes between - * them. - * - * @property name the log label, and the fastlane folder for the five Play surfaces. - * @property shots the shots in listing order; [naming] turns a position in it into a file name. - * @property committedDir where the `en-US` set lands, relative to the repository root - the one tracked in git. - * @property localeDir where every other locale lands, relative to `//`. - * @property mapZoom the map shot's zoom: the 1x layouts have far more room in both directions, so they sit half a level - * closer without any of the nine chips leaving the frame. - */ -internal data class FormFactor( - val name: String, - val widthPx: Int, - val heightPx: Int, - val density: Float, - val mapZoom: Double, - val shots: List, - val naming: FileNaming, - val committedDir: String, - val localeDir: String, -) { - fun fileName(shot: Shot): String = naming.fileName(shots.indexOf(shot), shot) -} - -/** How a form factor names its files: each surface's consumer reads a different pattern off the disk. */ -internal enum class FileNaming { - /** `1_messages`: Play and F-Droid sort the listing by file name. */ - Fastlane { - override fun fileName(index: Int, shot: Shot): String = "${index + 1}_${shot.slug}" - }, - - /** `meshtastic-desktop-01-nodes`: the names the Flathub metainfo's `` URLs point at. */ - Desktop { - override fun fileName(index: Int, shot: Shot): String = - "meshtastic-desktop-${(index + 1).toString().padStart(2, '0')}-${shot.slug}" - }, ; - - abstract fun fileName(index: Int, shot: Shot): String -} - -internal object FormFactors { - /** The Play listing, in listing order. */ - private val playShots = listOf(Shot.Messages, Shot.Nodes, Shot.Map, Shot.NodeDetail, Shot.Channels) - - /** - * A Play surface: `fastlane supply` and the Play API know phone, sevenInch and tenInch; chromebook and xr have no - * upload slot anywhere and are uploaded by hand in Play Console. Only `en-US` is committed, because everything - * under `fastlane/` is read straight from git by F-Droid and IzzyOnDroid. - */ - private fun play(folder: String, widthPx: Int, heightPx: Int, density: Float, mapZoom: Double) = FormFactor( - name = folder, - widthPx = widthPx, - heightPx = heightPx, - density = density, - mapZoom = mapZoom, - shots = playShots, - naming = FileNaming.Fastlane, - committedDir = "fastlane/metadata/android/en-US/images/$folder", - localeDir = "images/$folder", - ) - - /** 1080x1920 at 2.5x is 432x768 dp: a compact window, so the app lays out its bottom navigation bar. */ - val phone = play("phoneScreenshots", 1080, 1920, 2.5f, 11.5) - - /** - * 7-inch portrait. 1080x1920 at 1.8x is 600x1067 dp: the medium width class, where the app switches to a navigation - * rail but keeps one pane. Play's tablet rules want 9:16; a Nexus 7's own 1200x1920 is 5:8. - */ - val sevenInch = play("sevenInchScreenshots", 1080, 1920, 1.8f, 11.5) - - /** - * 10-inch landscape. 2560x1440 at 2x is 1280x720 dp: expanded, so list-detail and two-pane layouts split. Its map - * area is the shortest of the Play five, so it keeps the phone's zoom; one level closer puts the river node off the - * bottom edge. - */ - val tenInch = play("tenInchScreenshots", 2560, 1440, 2f, 11.5) - - /** A Chromebook at 1x: 1920x1080 dp, expanded. No supply or API slot; hand-uploaded in Play Console. */ - val chromebook = play("chromebookScreenshots", 1920, 1080, 1f, 12.0) - - /** Android XR's 8:5 panel at 1x: 1920x1200 dp, expanded, the same shell as the Chromebook. Hand-uploaded. */ - val xr = play("xrScreenshots", 1920, 1200, 1f, 12.0) - - /** - * The desktop app's Flathub listing: 1280x800 at 1x, AppStream's recommended 16:10, expanded, the same shell as the - * Chromebook. Its map area is the 10-inch's plus 80 dp, too little for the closer zoom. The metainfo lists the - * files by commit-pinned URL, so a regenerated set is two commits: the files, then the URLs. - */ - val desktop = - FormFactor( - name = "desktop", - widthPx = 1280, - heightPx = 800, - density = 1f, - mapZoom = 11.5, - shots = listOf(Shot.Nodes, Shot.Messages, Shot.Map, Shot.Connections, Shot.Settings), - naming = FileNaming.Desktop, - committedDir = "desktopApp/packaging/linux/screenshots", - localeDir = "desktop", - ) - - val all = listOf(phone, sevenInch, tenInch, chromebook, xr, desktop) -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt deleted file mode 100644 index ac552bfb75..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.ui.theme.AppTheme - -/** The framed phone canvas: 1242x2484 is Play's 2:1 long-side limit; drawn at 3x so the layout is written in dp. */ -internal object Frame { - const val WIDTH_PX = 1242 - const val HEIGHT_PX = 2484 - const val DENSITY = 3f -} - -/** - * The opt-in framed variant, for the website and social posts rather than the store: a phone [screen] inside a - * captioned bezel on the 1242x2484 canvas. Play itself forbids device frames in listing screenshots. Only the phone's - * shots come through here, and every one of them carries a caption. - */ -internal fun framePhone(screen: ImageBitmap, shot: Shot): ImageBitmap { - val title = requireNotNull(shot.captionTitle) { "$shot has no caption to frame" } - val description = requireNotNull(shot.captionDescription) { "$shot has no caption to frame" } - return renderScreen(Frame.WIDTH_PX, Frame.HEIGHT_PX, Frame.DENSITY) { StoreFrame(title, description, screen) } -} - -private val Background = Color(0xFF1F2937) -private val BezelColor = Color(0xFF0B0F14) -private val StatusBarColor = Color(0xFF111418) - -// The bezel's proportions, in reference dp: a 9:16 screen with a status bar above it and an even inset around both. -private const val SCREEN_W = 315f -private const val SCREEN_H = 560f -private const val STATUS_H = 24f -private const val INSET = 7f -private const val BEZEL_ASPECT = (SCREEN_W + 2 * INSET) / (STATUS_H + SCREEN_H + 2 * INSET) -private val BezelCorner = 34.dp - -// Tall enough for a two-line title plus a three-line description at the sizes above. -private val CAPTION_BLOCK_HEIGHT = 232.dp - -/** - * A captioned store frame: the headline and body copy across the top in the app's typography, then a phone bezel with a - * drawn status bar and [screen] scaled inside it, sized so the whole phone fits above the bottom edge. - */ -@Composable -private fun StoreFrame(title: StringResource, description: StringResource, screen: ImageBitmap) { - AppTheme(darkTheme = true, dynamicColor = false) { - Column( - modifier = Modifier.fillMaxSize().background(Background).padding(horizontal = 28.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - // The caption block has a fixed height whatever the text wraps to, so the bezel below it is the same - // size at the same place in all five shots; a shorter caption centres within the block. - Box(modifier = Modifier.fillMaxWidth().height(CAPTION_BLOCK_HEIGHT), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(title), - style = - MaterialTheme.typography.displaySmall.copy( - fontWeight = FontWeight.Bold, - lineHeight = 44.sp, - ), - color = Color.White, - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(14.dp)) - Text( - text = stringResource(description), - style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp, lineHeight = 27.sp), - color = Color.White.copy(alpha = 0.82f), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 8.dp), - ) - } - } - Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.TopCenter) { - PhoneBezel(screen) - } - Spacer(Modifier.height(24.dp)) - } - } -} - -@Composable -private fun PhoneBezel(screen: ImageBitmap) { - Column( - modifier = - Modifier.fillMaxHeight() - .aspectRatio(BEZEL_ASPECT, matchHeightConstraintsFirst = true) - .clip(RoundedCornerShape(BezelCorner)) - .background(BezelColor) - .padding(INSET.dp), - ) { - Column(modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(BezelCorner - INSET.dp))) { - StatusBar(modifier = Modifier.fillMaxWidth().weight(STATUS_H)) - Image( - bitmap = screen, - contentDescription = null, - modifier = Modifier.fillMaxWidth().weight(SCREEN_H), - contentScale = ContentScale.FillBounds, - ) - } - } -} - -/** A fixed 9:41 status bar with signal, Wi-Fi and a full battery, drawn rather than captured so it never varies. */ -@Composable -private fun StatusBar(modifier: Modifier = Modifier) { - Row( - modifier = modifier.background(StatusBarColor).padding(horizontal = 22.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "9:41", - style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), - color = Color.White, - ) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { - SignalGlyph() - WifiGlyph() - BatteryGlyph() - } - } -} - -@Composable -private fun SignalGlyph() { - Canvas(modifier = Modifier.size(width = 14.dp, height = 12.dp)) { - val path = - Path().apply { - moveTo(0f, size.height) - lineTo(size.width, 0f) - lineTo(size.width, size.height) - close() - } - drawPath(path, Color.White) - } -} - -@Composable -private fun WifiGlyph() { - Canvas(modifier = Modifier.size(width = 16.dp, height = 12.dp)) { - val stroke = Stroke(width = 2.dp.toPx()) - for (i in 0 until 3) { - val radius = size.width * (0.5f - i * 0.16f) - drawArc( - color = Color.White, - startAngle = 225f, - sweepAngle = 90f, - useCenter = false, - topLeft = Offset(size.width / 2 - radius, size.height - radius), - size = Size(radius * 2, radius * 2), - style = stroke, - ) - } - drawCircle(Color.White, radius = 1.5.dp.toPx(), center = Offset(size.width / 2, size.height - 1.dp.toPx())) - } -} - -@Composable -private fun BatteryGlyph() { - Canvas(modifier = Modifier.size(width = 24.dp, height = 12.dp)) { - val body = Size(size.width - 3.dp.toPx(), size.height) - drawRoundRect(Color.White, size = body, cornerRadius = CornerRadius(3.dp.toPx()), style = Stroke(1.5.dp.toPx())) - drawRoundRect( - Color.White, - topLeft = Offset(2.dp.toPx(), 2.dp.toPx()), - size = Size(body.width - 4.dp.toPx(), body.height - 4.dp.toPx()), - cornerRadius = CornerRadius(1.5.dp.toPx()), - ) - drawRoundRect( - Color.White, - topLeft = Offset(body.width + 1.dp.toPx(), size.height * 0.3f), - size = Size(2.dp.toPx(), size.height * 0.4f), - cornerRadius = CornerRadius(1.dp.toPx()), - ) - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt deleted file mode 100644 index 2ebcf1783a..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.unit.IntSize -import java.io.File -import java.util.Locale -import kotlin.math.ceil - -private const val NANOS_PER_MILLI = 1_000_000L -private const val COMMITTED_LOCALE = "en-US" -private const val ARG_REPO_DIR = 0 -private const val ARG_BUILD_DIR = 1 -private const val ARG_LOCALES = 2 -private const val ARG_FRAMED = 3 -private const val ARG_COUNT = 4 - -/** A BCP 47 tag as Crowdin and Play spell it: a language, then optional script, region or variant subtags. */ -private val LANGUAGE_TAG = Regex("^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$") - -/** - * Renders the listing screenshots: every [FormFactors.all] entry's shots, for each requested locale. - * - * Arguments: the repository root, the build output directory, a comma-separated locale list, and whether to also write - * the framed phone variants. `en-US` is written to each form factor's [FormFactor.committedDir] under the root; every - * other locale to `//` + its [FormFactor.localeDir], the same layout, for a later `fastlane supply`. - * Framed variants go to `/framed//`. - * - * The map is captured first, once per form factor, at the size the map screen lays its map area out at, and reused - * across locales: neither the basemap nor the chips are localized. Each locale then sets the JVM default locale, which - * is what the app's own strings, numbers and dates follow, and builds its own [SampleMesh] so the "last heard" labels - * are measured from a clock a few seconds old, before any of its screens are composed. - */ -fun main(args: Array) { - require(args.size == ARG_COUNT) { "usage: " } - val repoDir = File(args[ARG_REPO_DIR]) - val buildDir = File(args[ARG_BUILD_DIR]) - val locales = args[ARG_LOCALES].split(',').map { it.trim() }.filter { it.isNotEmpty() } - require(locales.isNotEmpty()) { "at least one locale is required, got '${args[ARG_LOCALES]}'" } - // Locales name output directories under the build dir, so only a language tag is accepted - nothing that could - // carry a path separator or a parent reference. - locales.forEach { require(LANGUAGE_TAG.matches(it)) { "'$it' is not a language tag such as en-US or pt-BR" } } - val framed = args[ARG_FRAMED].toBooleanStrict() - - val t0 = System.nanoTime() - val mapAreas = FormFactors.all.associateWith { it.mapArea() } - val mapSnapshots = MapSnapshot.capture(mapAreas.values.distinct(), SampleMesh()) - log("${mapSnapshots.size} map captures in ${ms(t0)} ms") - - for (locale in locales) { - Locale.setDefault(Locale.forLanguageTag(locale)) - val mesh = SampleMesh() - for (formFactor in FormFactors.all) { - val outDir = - if (locale == COMMITTED_LOCALE) { - File(repoDir, formFactor.committedDir) - } else { - File(buildDir, "$locale/${formFactor.localeDir}") - } - outDir.mkdirs() - val phoneShots = mutableMapOf() - for (shot in formFactor.shots) { - val t = System.nanoTime() - val snapshot = mapSnapshots.getValue(mapAreas.getValue(formFactor)) - val screen = formFactor.render(screen(shot, mesh, snapshot)) - val file = File(outDir, "${formFactor.fileName(shot)}.png") - screen.writePng(file) - log("$locale/${formFactor.name}/${file.name}: ${screen.width}x${screen.height} in ${ms(t)} ms") - if (framed && formFactor == FormFactors.phone) phoneShots[shot] = screen - } - if (phoneShots.isNotEmpty()) { - val framedDir = File(buildDir, "framed/$locale").apply { mkdirs() } - for ((shot, screen) in phoneShots) { - val t = System.nanoTime() - val frame = framePhone(screen, shot) - val file = File(framedDir, "${formFactor.fileName(shot)}.png") - frame.writePng(file) - log("framed/$locale/${file.name}: ${frame.width}x${frame.height} in ${ms(t)} ms") - } - } - } - } - log("done in ${ms(t0)} ms") -} - -private fun screen(shot: Shot, mesh: SampleMesh, mapSnapshot: ImageBitmap): @Composable () -> Unit = when (shot) { - Shot.Messages -> { - { MessagesScreen(mesh) } - } - - Shot.Nodes -> { - { NodesScreen(mesh) } - } - - Shot.Map -> { - { MapScreen(mesh, mapSnapshot) } - } - - Shot.NodeDetail -> { - { NodeDetailScreen(mesh) } - } - - Shot.Channels -> { - { ChannelsScreen(mesh) } - } - - Shot.Connections -> { - { ConnectionsScreen(mesh) } - } - - Shot.Settings -> { - { SettingsScreen() } - } -} - -private fun FormFactor.render(content: @Composable () -> Unit): ImageBitmap = - renderScreen(widthPx, heightPx, density, content) - -/** Lays the map screen out once with no image and reads back the map area, rounded up to whole dp. */ -private fun FormFactor.mapArea(): MapArea { - var area = IntSize.Zero - val mesh = SampleMesh() - render { MapScreen(mesh, snapshot = null, onMapArea = { area = it }) } - check(area != IntSize.Zero) { "$name: the map screen laid out no map area" } - return MapArea( - widthDp = ceil(area.width / density).toInt(), - heightDp = ceil(area.height / density).toInt(), - density = density, - zoom = mapZoom, - ) -} - -private fun ms(since: Long): Long = (System.nanoTime() - since) / NANOS_PER_MILLI - -private fun log(line: String) = System.err.println("[marketing-screenshots] $line") diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt deleted file mode 100644 index b6b03f2889..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.toPixelMap -import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.text.font.createFontFamilyResolver -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import kotlinx.io.files.Path -import org.maplibre.compose.camera.CameraPosition -import org.maplibre.compose.map.LocalMapState -import org.maplibre.compose.map.MapRuntime -import org.maplibre.compose.map.MapRuntimeOptions -import org.maplibre.compose.map.MapSnapshotRequest -import org.maplibre.compose.map.createMapRuntime -import org.maplibre.compose.style.BaseStyle -import org.maplibre.spatialk.geojson.Position -import org.meshtastic.feature.map.maplibre.layers.NodeLayers -import org.meshtastic.feature.map.maplibre.style.Basemaps -import java.nio.file.Files - -/** One capture: the map area in dp at the screen density, and the zoom the form factor asks for. */ -internal data class MapArea(val widthDp: Int, val heightDp: Int, val density: Float, val zoom: Double) - -/** - * Captures the real mesh map — the app's default basemap with its own [NodeLayers] chips over [SampleMesh] — through - * maplibre-compose's snapshotter, with no window and no display. Each [MapArea] is the map area between a form factor's - * app bar and its navigation at that screen's density, so the screen composable places it without rescaling. - */ -internal object MapSnapshot { - private const val CAPTURE_TIMEOUT_MS = 180_000L - private const val MAX_SETTLE_RUNTIMES = 4 - - fun capture(areas: List, mesh: SampleMesh): Map = - areas.associateWith { area -> captureUntilStable(area, mesh) } - - /** - * Label and glyph placement depends on the order tiles arrive, which the network decides on the first load, so a - * capture can differ from the next run's by a few pixels; a second capture from the same runtime always matches the - * first, because that runtime already holds the placement. So each capture comes from a fresh runtime over one - * shared tile cache: the first fills the cache from the network, the ones after it read the cache in a stable - * order, and two consecutive runtimes agreeing means the run is reproducible. A warm runtime costs about a second. - */ - private fun captureUntilStable(area: MapArea, mesh: SampleMesh): ImageBitmap = runBlocking { - val cacheDir = Files.createTempDirectory("marketing-maplibre") - val cacheFile = Path(cacheDir.resolve("cache.db").toString()) - try { - var previous = captureOnce(cacheFile, area, mesh) - repeat(MAX_SETTLE_RUNTIMES - 1) { - val next = captureOnce(cacheFile, area, mesh) - if (next.toPixelMap().buffer.contentEquals(previous.toPixelMap().buffer)) return@runBlocking next - previous = next - } - System.err.println( - "[marketing-screenshots] warning: map ${area.widthDp}x${area.heightDp}@${area.density} never settled", - ) - previous - } finally { - cacheDir.toFile().deleteRecursively() - } - } - - private suspend fun captureOnce(cacheFile: Path, area: MapArea, mesh: SampleMesh): ImageBitmap { - val runtime = createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile)) - return try { - withTimeout(CAPTURE_TIMEOUT_MS) { runtime.capture(area, mesh) } - } finally { - withContext(NonCancellable) { - runtime.close() - runtime.awaitClosed() - } - } - } - - /** - * The app's default basemap, labels included: a street map with no names is not the in-app experience Play asks - * for. MapLibre packs glyphs into an atlas in the order tiles arrive, so on the wide layouts a label's antialiased - * edge can land one level off between generations - a dozen pixels, invisible - which is why [captureUntilStable] - * warns rather than fails when two runtimes disagree, and why a regenerated wide map may not `cmp` the last one. - */ - private val baseStyle: BaseStyle = BaseStyle.Uri(Basemaps.Liberty.styleUri) - - private suspend fun MapRuntime.capture(area: MapArea, mesh: SampleMesh): ImageBitmap { - // NodeLayers reads LocalMapState (for cluster clicks) and rasterizes chips with a TextMeasurer; the - // snapshotter's own composition provides neither, so both are supplied here. - val mapState = createMapState(baseStyle) - val fontResolver = createFontFamilyResolver() - val snapshotter = - createSnapshotter(baseStyle) { - CompositionLocalProvider( - LocalMapState provides mapState, - LocalFontFamilyResolver provides fontResolver, - ) { - NodeLayers( - nodes = mesh.nodes, - myNodeNum = mesh.baseCamp.num, - showPrecisionCircles = false, - onNodeClick = {}, - onClusterZoom = { _, _ -> }, - onClusterMembers = {}, - visibleBounds = null, - zoom = area.zoom.toInt(), - ) - } - } - return try { - val request = - MapSnapshotRequest( - width = area.widthDp, - height = area.heightDp, - cameraPosition = - CameraPosition( - target = Position(longitude = SampleMesh.CENTER_LON, latitude = SampleMesh.CENTER_LAT), - zoom = area.zoom, - ), - density = area.density, - ) - snapshotter.capture(request) - } finally { - withContext(NonCancellable) { - snapshotter.close() - snapshotter.awaitClosed() - } - } - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt deleted file mode 100644 index 758018c9b0..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.model.Channel -import org.meshtastic.core.model.Message -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.channels -import org.meshtastic.core.resources.conversations -import org.meshtastic.core.resources.direct_messages -import org.meshtastic.core.resources.map -import org.meshtastic.core.resources.send -import org.meshtastic.core.resources.type_a_message -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.Send -import org.meshtastic.feature.messaging.component.MessageItem -import org.meshtastic.feature.messaging.component.MessageTopBar -import org.meshtastic.feature.messaging.ui.contact.ContactItem - -/** - * The messages tab: the LongTurbo thread, and beside it on an expanded window the conversation list the app shows in - * the list pane. A compact window shows the thread alone, as the app does once a conversation is open. - */ -@Composable -internal fun MessagesScreen(mesh: SampleMesh) { - MarketingTheme { - AppShell(TopLevelDestination.Messages) { - ListDetail(compactPane = Pane.Detail, list = { ConversationsPane(mesh) }, detail = { ThreadPane(mesh) }) - } - } -} - -/** The conversation list from the messaging feature's own [ContactItem] rows under its own app bar. */ -@Composable -private fun ConversationsPane(mesh: SampleMesh) { - val (channels, direct) = mesh.contacts.partition { it.contactKey.endsWith("^all") } - val openThread = mesh.contacts.first().contactKey - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.conversations), - ourNode = mesh.baseCamp, - showNodeChip = true, - canNavigateUp = false, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { - item { SectionHeader(stringResource(Res.string.channels), channels.size) } - items(channels, key = { it.contactKey }) { contact -> - ContactItem( - contact = contact.copy(lastMessageText = previewLine(mesh, contact.contactKey)), - selected = false, - isActive = contact.contactKey == openThread, - channels = mesh.channelSet, - ) - } - item { SectionHeader(stringResource(Res.string.direct_messages), direct.size) } - items(direct, key = { it.contactKey }) { contact -> - ContactItem( - contact = contact.copy(lastMessageText = previewLine(mesh, contact.contactKey)), - selected = false, - channels = mesh.channelSet, - ) - } - } - } -} - -/** The app prefixes a received preview with the sender's short name and leaves our own bare. */ -@Composable -private fun previewLine(mesh: SampleMesh, contactKey: String): String { - val (sender, line) = mesh.contactPreview.getValue(contactKey) - val text = stringResource(line) - return if (sender == null) text else "${sender.user.short_name}: $text" -} - -/** The LongTurbo channel thread, built from the messaging feature's own bubbles, reactions and top bar. */ -@Composable -private fun ThreadPane(mesh: SampleMesh) { - val channelName = Channel(mesh.channelSet.settings.first(), mesh.channelSet.lora_config!!).name - val messages = mesh.messages.map { it.copy(text = stringResource(mesh.messageText.getValue(it.uuid))) } - Scaffold( - topBar = { - MessageTopBar( - title = channelName, - channelIndex = 0, - mismatchKey = false, - onNavigateBack = {}, - channels = mesh.channelSet, - channelIndexParam = 0, - showQuickChat = false, - onToggleQuickChat = {}, - ) - }, - bottomBar = { Composer() }, - ) { padding -> - // The app's list is reversed and anchored at the newest message, so a short window shows the end of the - // thread; the grouping rule still reads in thread order. - val newestFirst = messages.asReversed() - LazyColumn( - modifier = Modifier.fillMaxSize().padding(padding), - contentPadding = PaddingValues(vertical = 8.dp), - reverseLayout = true, - ) { - itemsIndexed(newestFirst, key = { _, message -> message.uuid }) { reversedIndex, message -> - val index = messages.lastIndex - reversedIndex - val prevSame = index > 0 && sameGroup(messages[index - 1], message) - val nextSame = index < messages.lastIndex && sameGroup(message, messages[index + 1]) - MessageItem( - message = message, - node = message.node, - ourNode = mesh.baseCamp, - selected = false, - showUserName = !prevSame, - hasSamePrev = prevSame, - hasSameNext = nextSame, - emojis = mesh.reactions[message.uuid].orEmpty(), - ) - } - } - } -} - -/** The list's own grouping rule: one sender, one direction, within ten minutes. The feature's copy is internal. */ -private fun sameGroup(older: Message, newer: Message): Boolean = older.fromLocal == newer.fromLocal && - older.node.num == newer.node.num && - newer.receivedTime - older.receivedTime < 10 * 60_000L - -/** - * The composer row. The feature's `MessageInput` is internal to `feature:messaging`, so this is the same shape drawn - * from Material 3 primitives: an outlined field with the real placeholder string and the real send icon. - */ -@Composable -private fun Composer() { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - value = "", - onValueChange = {}, - modifier = Modifier.weight(1f), - placeholder = { Text(stringResource(Res.string.type_a_message)) }, - shape = MaterialTheme.shapes.extraLarge, - singleLine = true, - ) - IconButton(onClick = {}) { - Icon( - imageVector = MeshtasticIcons.Send, - contentDescription = stringResource(Res.string.send), - tint = MaterialTheme.colorScheme.primary, - ) - } - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt deleted file mode 100644 index dd1b687248..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.common.util.MeasurementSystem -import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.Node -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.nodes -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.core.ui.component.NodeItem -import org.meshtastic.feature.node.detail.NodeDetailContent -import org.meshtastic.feature.node.detail.NodeDetailUiState -import org.meshtastic.feature.node.model.LogsType -import org.meshtastic.feature.node.model.MetricsState - -/** - * The nodes tab: the list, and beside it on an expanded window a node's detail page. Summit Solar sits in the detail - * pane here so this shot and [NodeDetailScreen], which opens Ridge Top, are two different pages on a wide window. - */ -@Composable -internal fun NodesScreen(mesh: SampleMesh) { - MarketingTheme { - AppShell(TopLevelDestination.Nodes) { - ListDetail( - compactPane = Pane.List, - list = { NodesPane(mesh) }, - detail = { NodeDetailPane(mesh, mesh.summitSolar) }, - ) - } - } -} - -/** Ridge Top's detail page, with the list beside it on an expanded window. */ -@Composable -internal fun NodeDetailScreen(mesh: SampleMesh) { - MarketingTheme { - AppShell(TopLevelDestination.Nodes) { - ListDetail( - compactPane = Pane.Detail, - list = { NodesPane(mesh) }, - detail = { NodeDetailPane(mesh, mesh.ridgeTop) }, - ) - } - } -} - -/** The node list as the app shows it: our node's chip in the bar, one [NodeItem] card per node. */ -@Composable -private fun NodesPane(mesh: SampleMesh) { - val ourNode = mesh.baseCamp - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.nodes), - ourNode = ourNode, - showNodeChip = true, - canNavigateUp = false, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - LazyColumn(modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 8.dp)) { - items(mesh.nodes, key = { it.num }) { node -> - NodeItem( - thisNode = ourNode, - thatNode = node, - distanceUnits = MeasurementSystem.METRIC, - tempInFahrenheit = false, - connectionState = ConnectionState.Connected, - isActive = node.num == ourNode.num, - ) - } - } - } -} - -/** A remote, unmanaged node's page from the node feature's own [NodeDetailContent]. */ -@Composable -private fun NodeDetailPane(mesh: SampleMesh, node: Node) { - Scaffold( - topBar = { - MainAppBar( - title = node.user.long_name, - ourNode = mesh.baseCamp, - showNodeChip = false, - canNavigateUp = true, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - NodeDetailContent( - uiState = - NodeDetailUiState( - node = node, - ourNode = mesh.baseCamp, - metricsState = MetricsState(isLocal = false, isManaged = false), - availableLogs = - setOf( - LogsType.DEVICE, - LogsType.POSITIONS, - LogsType.ENVIRONMENT, - LogsType.SIGNAL, - LogsType.TRACEROUTE, - ), - ), - onAction = {}, - onFirmwareSelect = {}, - onSaveNotes = { _, _ -> }, - modifier = Modifier.padding(padding), - ) - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt deleted file mode 100644 index fc53a80dd5..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -import androidx.compose.runtime.Composable -import androidx.compose.ui.ImageComposeScene -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asSkiaBitmap -import androidx.compose.ui.graphics.toComposeImageBitmap -import androidx.compose.ui.unit.Density -import androidx.compose.ui.use -import org.jetbrains.skia.EncodedImageFormat -import org.jetbrains.skia.Image -import java.io.File - -/** Bounded settle: frames rendered until the scene reports no pending invalidation, or this many at most. */ -private const val MAX_FRAMES = 30 -private const val FRAME_NANOS = 16_000_000L - -/** - * Step one of the pipeline, form-factor agnostic: composes [content] offscreen at the given pixel size and density and - * returns the settled frame. A phone screen, a desktop window and a store frame all come through here. - */ -internal fun renderScreen(width: Int, height: Int, density: Float, content: @Composable () -> Unit): ImageBitmap = - ImageComposeScene(width = width, height = height, density = Density(density), content = content).use { scene -> - var nanos = 0L - var frames = 1 - var image = scene.render(nanos) - while (scene.hasInvalidations() && frames < MAX_FRAMES) { - nanos += FRAME_NANOS - val next = scene.render(nanos) - // Each superseded frame is a native Skia image; the last one is owned by the returned bitmap. - image.close() - image = next - frames++ - } - if (scene.hasInvalidations()) { - System.err.println("[marketing-screenshots] warning: scene still invalidating after $frames frames") - } - image.toComposeImageBitmap() - } - -internal fun ImageBitmap.writePng(file: File) { - Image.makeFromBitmap(asSkiaBitmap()).use { image -> - checkNotNull(image.encodeToData(EncodedImageFormat.PNG)).use { data -> file.writeBytes(data.bytes) } - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt deleted file mode 100644 index 0401e31bfc..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import okio.ByteString.Companion.toByteString -import org.jetbrains.compose.resources.StringResource -import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.model.Contact -import org.meshtastic.core.model.ContactKey -import org.meshtastic.core.model.Message -import org.meshtastic.core.model.MessageStatus -import org.meshtastic.core.model.Node -import org.meshtastic.core.model.Reaction -import org.meshtastic.proto.ChannelSet -import org.meshtastic.proto.ChannelSettings -import org.meshtastic.proto.Config -import org.meshtastic.proto.DeviceMetadata -import org.meshtastic.proto.DeviceMetrics -import org.meshtastic.proto.EnvironmentMetrics -import org.meshtastic.proto.HardwareModel -import org.meshtastic.proto.Position -import org.meshtastic.proto.PowerMetrics -import org.meshtastic.proto.User -import org.meshtastic.screenshot.marketing.resources.Res -import org.meshtastic.screenshot.marketing.resources.marketing_msg_crossing_low -import org.meshtastic.screenshot.marketing.resources.marketing_msg_heading_up -import org.meshtastic.screenshot.marketing.resources.marketing_msg_lunch_lookout -import org.meshtastic.screenshot.marketing.resources.marketing_msg_made_ridge -import org.meshtastic.screenshot.marketing.resources.marketing_msg_parked -import org.meshtastic.screenshot.marketing.resources.marketing_msg_take_crossing -import org.meshtastic.screenshot.marketing.resources.marketing_msg_truck_around -import org.meshtastic.screenshot.marketing.resources.marketing_msg_weather_window -import org.meshtastic.screenshot.marketing.resources.marketing_preview_first_aid -import org.meshtastic.screenshot.marketing.resources.marketing_preview_see_me_on_map -import org.meshtastic.screenshot.marketing.resources.marketing_preview_spare_battery -import org.meshtastic.screenshot.marketing.resources.marketing_preview_trail_crew -import org.meshtastic.screenshot.marketing.resources.marketing_preview_weather_thanks -import java.time.LocalDate -import java.time.ZoneId - -/** - * One plausible mesh shared by every store screenshot, so the five shots tell one story: a base camp, a ridge-top - * router, and a handful of hikers, boats and trucks spread across a valley outside Dallas. Nine nodes, so the map never - * clusters them. "Last heard" and message timestamps hang off the wall clock because the row composables format them - * relative to now. - * - * Node and channel names are literals: they are what a real group typed into their radios, and the channel names feed - * the QR payload. The prose - the thread, the direct-message previews - is a string resource per line, resolved at - * composition so each locale renders its own translation once Crowdin has filled it in. - * - * Built once per render pass: "last heard" hangs off [now], the wall clock when the mesh was built, and the row - * composables format it against the wall clock at composition. [heardAgo] places every offset mid-minute, so a label - * can only change once composition falls more than 30 s behind construction; one locale's pass takes a third of that. - */ -internal class SampleMesh(private val now: Int = (nowMillis / 1_000L).toInt()) { - companion object { - const val CENTER_LAT = 32.7767 - const val CENTER_LON = -96.797 - - /** The firmware every radio in the group runs; the routers report it, and the connections card shows it. */ - const val FIRMWARE_VERSION = "2.8.1.f3c2a9" - private const val HALF_MINUTE_SECONDS = 30 - private const val JUST_NOW_SECONDS = 5 - } - - /** 0 reads "Now"; anything else lands 30 s into that minute so a delayed composition cannot move the label. */ - private fun heardAgo(minutes: Int): Int = if (minutes == 0) JUST_NOW_SECONDS else minutes * 60 + HALF_MINUTE_SECONDS - - /** A fixed, obviously synthetic 256-bit key so the detail page shows a keyed node rather than a key warning. */ - private val ridgeKey = ByteArray(32) { i -> (i * 13 + 29).toByte() }.toByteString() - private val summitKey = ByteArray(32) { i -> (i * 17 + 41).toByte() }.toByteString() - - val baseCamp = - node( - 0x2b3c4d5e, - "Base Camp", - "BASE", - HardwareModel.HELTEC_V3, - Role.CLIENT, - 95, - 0, - 0, - 0.0, - 0.0, - 0f, - 0, - favorite = true, - ) - val ridgeTop = - node( - 0x1a2b3c4d, - "Ridge Top", - "RDGE", - HardwareModel.RAK4631, - Role.ROUTER, - 88, - 0, - 2, - 0.04, - 0.03, - 11.5f, - -68, - favorite = true, - ) - .let { base -> - base.copy(user = base.user.newBuilder().also { it.public_key = ridgeKey }.build(), publicKey = ridgeKey) - } - .copy( - metadata = - DeviceMetadata.Builder() - .also { - it.firmware_version = FIRMWARE_VERSION - it.hw_model = HardwareModel.RAK4631 - it.role = Role.ROUTER - it.hasBluetooth = true - it.hasWifi = false - it.canShutdown = true - } - .build(), - environmentMetrics = - EnvironmentMetrics.Builder() - .also { - it.temperature = 17.4f - it.relative_humidity = 41f - it.barometric_pressure = 862.7f - } - .build(), - deviceMetrics = - DeviceMetrics.Builder() - .also { - it.battery_level = 88 - it.voltage = 4.02f - it.channel_utilization = 6.8f - it.air_util_tx = 2.3f - it.uptime_seconds = 19 * 86_400 + 7 * 3_600 - } - .build(), - ) - val trailhead = - node(0x3c4d5e6f, "Trailhead", "TRLH", HardwareModel.TBEAM, Role.CLIENT, 72, 1, 3, -0.03, 0.045, 8.25f, -92) - val riverCrossing = - node( - 0x4d5e6f70, - "River Crossing", - "RIVR", - HardwareModel.T_ECHO, - Role.CLIENT_MUTE, - 64, - 1, - 7, - -0.045, - -0.02, - 6.0f, - -101, - ) - - /** A solar router two hops out: the second node with a full detail page, so the wide layouts show two of them. */ - val summitSolar = - node( - 0x5e6f7081, - "Summit Solar", - "SMMT", - HardwareModel.RAK4631, - Role.ROUTER, - 91, - 2, - 15, - 0.05, - -0.04, - 3.5f, - -110, - favorite = true, - ) - .let { base -> - base.copy( - user = base.user.newBuilder().also { it.public_key = summitKey }.build(), - publicKey = summitKey, - ) - } - .copy( - metadata = - DeviceMetadata.Builder() - .also { - it.firmware_version = FIRMWARE_VERSION - it.hw_model = HardwareModel.RAK4631 - it.role = Role.ROUTER - it.hasBluetooth = true - it.hasWifi = false - it.canShutdown = true - } - .build(), - environmentMetrics = - EnvironmentMetrics.Builder() - .also { - it.temperature = 9.6f - it.relative_humidity = 58f - it.barometric_pressure = 791.3f - } - .build(), - powerMetrics = - PowerMetrics.Builder() - .also { - it.ch1_voltage = 13.8f - it.ch1_current = 412f - } - .build(), - deviceMetrics = - DeviceMetrics.Builder() - .also { - it.battery_level = 91 - it.voltage = 4.09f - it.channel_utilization = 5.1f - it.air_util_tx = 1.9f - it.uptime_seconds = 63 * 86_400 + 2 * 3_600 - } - .build(), - ) - val kayakDan = - node( - 0x6f708192, - "Kayak Dan", - "KDAN", - HardwareModel.HELTEC_WIRELESS_TRACKER, - Role.TRACKER, - 58, - 2, - 25, - 0.012, - -0.042, - 2.75f, - -113, - ) - val fireLookout = - node( - 0x708192a3, - "Old Fire Lookout", - "LOOK", - HardwareModel.TBEAM, - Role.CLIENT, - 47, - 3, - 60, - -0.01, - 0.02, - -1.5f, - -118, - ) - val sarahsTruck = - node( - 0x8192a3b4.toInt(), - "Sarah's Truck", - "SRAH", - HardwareModel.T_DECK, - Role.CLIENT, - 83, - 1, - 4, - 0.025, - 0.01, - 9.0f, - -85, - ) - val hamShack = - node( - 0xa3b4c5d6.toInt(), - "Ham Shack", - "SHCK", - HardwareModel.STATION_G2, - Role.CLIENT, - 77, - 2, - 11, - -0.02, - -0.035, - 4.5f, - -104, - ) - - /** The node list, in the order the real list would show it: us first, then favorites, then by last heard. */ - val nodes: List = - listOf(baseCamp, ridgeTop, summitSolar, trailhead, sarahsTruck, riverCrossing, hamShack, kayakDan, fireLookout) - - /** LongTurbo (index 0, default PSK), a private group channel with a full 256-bit key, and a second private one. */ - val channelSet: ChannelSet = - ChannelSet.Builder() - .also { set -> - set.settings = - listOf( - ChannelSettings.Builder().also { it.psk = byteArrayOf(1).toByteString() }.build(), - ChannelSettings.Builder() - .also { - it.name = "Basecamp" - it.psk = ByteArray(32) { i -> (i * 7 + 3).toByte() }.toByteString() - } - .build(), - ChannelSettings.Builder() - .also { - it.name = "TrailCrew" - it.psk = ByteArray(32) { i -> (i * 11 + 5).toByte() }.toByteString() - } - .build(), - ) - set.lora_config = - Config.LoRaConfig.Builder() - .also { - it.use_preset = true - it.modem_preset = Config.LoRaConfig.ModemPreset.LONG_TURBO - it.region = Config.LoRaConfig.RegionCode.US - it.hop_limit = 3 - it.tx_enabled = true - } - .build() - } - .build() - - private val messageTextByUuid = mutableMapOf() - - /** - * The LongTurbo thread as seen from Base Camp: a day trip checking in from the ridge and the river. Each - * [Message.text] is empty here; [messageText] carries the line as a resource, and the screen fills it in at - * composition. - */ - val messages: List = - listOf( - received(trailhead, Res.string.marketing_msg_heading_up, "09:02", 39, 5.5f, -92, hops = 1), - received(sarahsTruck, Res.string.marketing_msg_parked, "09:05", 36, 9.0f, -85, hops = 1), - sent(Res.string.marketing_msg_weather_window, "09:07", 34), - received(trailhead, Res.string.marketing_msg_made_ridge, "09:31", 10, 11.25f, -70, hops = 1) - .withReactions(reaction(baseCamp, "👍", 9), reaction(sarahsTruck, "🔥", 8)), - received(kayakDan, Res.string.marketing_msg_crossing_low, "09:34", 7, 2.75f, -113, hops = 2), - sent(Res.string.marketing_msg_take_crossing, "09:35", 6), - received(sarahsTruck, Res.string.marketing_msg_truck_around, "09:38", 3, 8.5f, -88, hops = 1) - .withReactions(reaction(trailhead, "❤️", 2)), - received(trailhead, Res.string.marketing_msg_lunch_lookout, "09:41", 0, 10.0f, -74, hops = 1), - ) - - /** The prose of each message in [messages], by uuid. */ - val messageText: Map - get() = messageTextByUuid - - /** - * The conversation list beside the thread on wide layouts: the three channels, then the hikers Base Camp has - * messaged directly. [Contact.lastMessageText] is empty here for the same reason as [Message.text]; the preview - * line is in [contactPreview]. - */ - val contacts: List = - listOf( - channelContact(0, "LongTurbo", at = "09:41", unread = 0), - channelContact(1, "Basecamp", at = "09:23", unread = 0), - channelContact(2, "TrailCrew", at = "08:55", unread = 2), - directContact(trailhead, at = "09:29"), - directContact(sarahsTruck, at = "09:14", unread = 1), - directContact(kayakDan, at = "08:46"), - ) - - /** - * The preview line of each contact in [contacts], by contact key: the sender whose short name the app prefixes the - * line with (none when Base Camp sent it), and the line itself. - */ - val contactPreview: Map> = - mapOf( - contacts[0].contactKey to (trailhead to Res.string.marketing_msg_lunch_lookout), - contacts[1].contactKey to (sarahsTruck to Res.string.marketing_preview_spare_battery), - contacts[2].contactKey to (trailhead to Res.string.marketing_preview_trail_crew), - contacts[3].contactKey to (trailhead to Res.string.marketing_preview_weather_thanks), - contacts[4].contactKey to (null to Res.string.marketing_preview_first_aid), - contacts[5].contactKey to (kayakDan to Res.string.marketing_preview_see_me_on_map), - ) - - /** - * The row shows a message from today as its clock time, so these are fixed times of day rather than offsets from - * now, in step with the thread's own "09:02" stamps; an offset would print the wall clock. - */ - private fun todayAt(time: String): Long { - val (hour, minute) = time.split(':').map(String::toInt) - return LocalDate.now().atTime(hour, minute).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - } - - private fun channelContact(index: Int, name: String, at: String, unread: Int): Contact = Contact( - contactKey = ContactKey.broadcast(index).value, - shortName = index.toString(), - longName = name, - lastMessageTime = todayAt(at), - lastMessageText = "", - unreadCount = unread, - messageCount = messages.size, - isMuted = false, - isUnmessageable = false, - ) - - private fun directContact(from: Node, at: String, unread: Int = 0): Contact = Contact( - contactKey = "0${from.user.id}", - shortName = from.user.short_name, - longName = from.user.long_name, - lastMessageTime = todayAt(at), - lastMessageText = "", - unreadCount = unread, - messageCount = 4, - isMuted = false, - isUnmessageable = false, - nodeColors = from.colors, - ) - - /** Reactions render from `MessageItem`'s own list, not [Message.emojis], so they are carried per message here. */ - val reactions: Map> = messages.associate { it.uuid to it.emojis } - - private fun Message.withReactions(vararg reactions: Reaction): Message = copy(emojis = reactions.toList()) - - private fun reaction(from: Node, emoji: String, minutesAgo: Int): Reaction = Reaction( - replyId = 0, - user = from.user, - emoji = emoji, - timestamp = nowMillis - minutesAgo * 60_000L, - snr = 6.0f, - rssi = -90, - hopsAway = from.hopsAway, - ) - - private var nextMessageId = 0L - - private fun received( - from: Node, - text: StringResource, - time: String, - minutesAgo: Int, - snr: Float, - rssi: Int, - hops: Int, - ): Message = message( - from, - text, - time, - minutesAgo, - fromLocal = false, - status = MessageStatus.RECEIVED, - snr = snr, - rssi = rssi, - hops = hops, - ) - - private fun sent(text: StringResource, time: String, minutesAgo: Int): Message = message( - baseCamp, - text, - time, - minutesAgo, - fromLocal = true, - status = MessageStatus.DELIVERED, - snr = null, - rssi = null, - hops = 0, - ) - - private fun message( - from: Node, - text: StringResource, - time: String, - minutesAgo: Int, - fromLocal: Boolean, - status: MessageStatus, - snr: Float?, - rssi: Int?, - hops: Int, - ): Message { - val id = ++nextMessageId - messageTextByUuid[id] = text - return Message( - uuid = id, - receivedTime = nowMillis - minutesAgo * 60_000L, - node = from, - text = "", - fromLocal = fromLocal, - time = time, - read = true, - status = status, - routingError = 0, - packetId = 0x5a00 + id.toInt(), - emojis = emptyList(), - snr = snr, - rssi = rssi, - hopsAway = hops, - replyId = null, - ) - } - - private fun node( - num: Int, - longName: String, - shortName: String, - hw: HardwareModel, - role: Role, - battery: Int, - hops: Int, - heardMinutesAgo: Int, - dLat: Double, - dLon: Double, - snr: Float, - rssi: Int, - favorite: Boolean = false, - ): Node = Node( - num = num, - user = - User.Builder() - .also { - it.id = "!${num.toUInt().toString(16).padStart(8, '0')}" - it.long_name = longName - it.short_name = shortName - it.hw_model = hw - it.role = role - } - .build(), - position = - Position.Builder() - .also { - it.latitude_i = ((CENTER_LAT + dLat) * 1e7).toInt() - it.longitude_i = ((CENTER_LON + dLon) * 1e7).toInt() - it.altitude = 120 + (num and 0xff) - it.sats_in_view = 8 - it.time = now - heardAgo(heardMinutesAgo) - } - .build(), - lastHeard = now - heardAgo(heardMinutesAgo), - channel = 0, - snr = snr, - rssi = rssi, - deviceMetrics = - DeviceMetrics.Builder() - .also { - it.battery_level = battery - it.voltage = 3.6f + battery / 250f - it.channel_utilization = 4.2f - it.air_util_tx = 1.1f - it.uptime_seconds = 86_400 - } - .build(), - hopsAway = hops, - isFavorite = favorite, - ) -} - -private typealias Role = Config.DeviceConfig.Role diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt deleted file mode 100644 index ab9d717588..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MagicNumber") - -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.model.Channel -import org.meshtastic.core.model.util.getChannelUrl -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.channels -import org.meshtastic.core.resources.edit -import org.meshtastic.core.resources.generate_qr_code -import org.meshtastic.core.resources.map -import org.meshtastic.core.resources.qr_code -import org.meshtastic.core.resources.share_qr_subtext -import org.meshtastic.core.ui.component.AdaptiveTwoPane -import org.meshtastic.core.ui.component.ChannelSelection -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.QrCode -import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.core.ui.util.rememberQrCodePainter -import org.meshtastic.feature.map.component.MapControlsOverlay -import org.meshtastic.feature.map.component.MapZoomControls -import org.meshtastic.proto.Config -import org.meshtastic.screenshot.marketing.resources.marketing_map_attribution -import org.meshtastic.screenshot.marketing.resources.Res as MarketingRes - -/** Every store screenshot is the dark theme, matching the live listing; dynamic color would follow the host. */ -@Composable -internal fun MarketingTheme(content: @Composable () -> Unit) { - AppTheme(darkTheme = true, dynamicColor = false) { Surface(modifier = Modifier.fillMaxSize(), content = content) } -} - -/** - * The map screen: the real MapLibre snapshot (basemap plus the app's own node chips, captured by [MapSnapshot]) under - * the map screen's real app bar, toolbar and zoom controls. The library draws its attribution as a MapView overlay, - * which the snapshotter has no equivalent of, so the same credit line is written here. - * - * Composed once per form factor with no [snapshot] to measure the map area - [onMapArea] reports it in pixels - and - * again with the capture made at exactly that size, so the image is placed without rescaling. - */ -@Composable -internal fun MapScreen(mesh: SampleMesh, snapshot: ImageBitmap?, onMapArea: (IntSize) -> Unit = {}) { - MarketingTheme { - AppShell(TopLevelDestination.Map) { - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.map), - ourNode = mesh.baseCamp, - showNodeChip = true, - canNavigateUp = false, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - Box(modifier = Modifier.fillMaxSize().padding(padding).onSizeChanged(onMapArea)) { - if (snapshot != null) { - Image( - bitmap = snapshot, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - } - MapControlsOverlay( - modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp), - onToggleFilterMenu = {}, - onSitePlannerClick = {}, - onToggleLocationTracking = {}, - ) - MapZoomControls( - onZoomIn = {}, - onZoomOut = {}, - modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), - ) - Surface( - modifier = Modifier.align(Alignment.BottomStart).padding(8.dp), - color = Color.White.copy(alpha = 0.8f), - shape = MaterialTheme.shapes.small, - ) { - Text( - text = stringResource(MarketingRes.string.marketing_map_attribution), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelSmall, - color = Color.Black, - ) - } - } - } - } - } -} - -/** - * The channel list with the share affordance, in the same [AdaptiveTwoPane] the app's channel screen uses: the rows and - * the edit button first, the QR second, stacked on a phone and side by side on an expanded window. The real screen - * shows the QR in a dialog window, which an offscreen scene does not compose, so the same QR painter is laid out - * inline. - */ -@Composable -internal fun ChannelsScreen(mesh: SampleMesh) { - val channelSet = mesh.channelSet - val loraConfig = channelSet.lora_config ?: Config.LoRaConfig.Builder().build() - val presetName = Channel(loraConfig = loraConfig).name - MarketingTheme { - AppShell(TopLevelDestination.Settings) { - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.channels), - ourNode = mesh.baseCamp, - showNodeChip = false, - canNavigateUp = true, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - AdaptiveTwoPane( - modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 24.dp, vertical = 16.dp), - first = { - channelSet.settings.forEachIndexed { index, settings -> - ChannelSelection( - index = index, - title = settings.name.ifEmpty { presetName }, - enabled = true, - isSelected = true, - onSelected = {}, - channel = Channel(settings, loraConfig), - ) - } - OutlinedButton( - onClick = {}, - modifier = Modifier.fillMaxWidth(), - colors = - ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), - ) { - Text(text = stringResource(Res.string.edit)) - } - }, - second = { ShareCard(channelSet.getChannelUrl().toString()) }, - ) - } - } - } -} - -@Composable -private fun ShareCard(url: String) { - val qrSizePx = with(LocalDensity.current) { 200.dp.roundToPx() } - val qrPainter = rememberQrCodePainter(remember(url) { url }, qrSizePx) - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = {}, modifier = Modifier.padding(16.dp)) { - Icon(imageVector = MeshtasticIcons.QrCode, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(Res.string.generate_qr_code)) - } - Surface(shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainerHigh) { - Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Image( - painter = qrPainter, - contentDescription = stringResource(Res.string.qr_code), - modifier = Modifier.size(200.dp), - contentScale = ContentScale.Fit, - ) - Text( - text = stringResource(Res.string.share_qr_subtext), - modifier = Modifier.fillMaxWidth().padding(top = 12.dp), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - } - } - Spacer(modifier = Modifier.height(8.dp)) - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt deleted file mode 100644 index 4fa410b557..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.bottom_nav_settings -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.settings.radio.RadioConfigItemList -import org.meshtastic.feature.settings.radio.RadioConfigState - -/** - * The settings tab as the desktop app lays it out: `DesktopSettingsScreen`'s app bar and scrolling column, which needs - * two ViewModels, over the settings feature's own [RadioConfigItemList] - the configuration, backup and advanced - * sections a connected local radio gets. The app-level sections that follow them fall below an 800 px window. - */ -@Composable -internal fun SettingsScreen() { - MarketingTheme { - AppShell(TopLevelDestination.Settings) { - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.bottom_nav_settings), - ourNode = null, - showNodeChip = false, - canNavigateUp = false, - onNavigateUp = {}, - onClickChip = {}, - actions = {}, - ) - }, - ) { padding -> - Column( - modifier = Modifier.padding(padding).verticalScroll(rememberScrollState()).padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - RadioConfigItemList( - state = RadioConfigState(isLocal = true, connected = true), - isManaged = false, - isOtaCapable = false, - onNavigate = {}, - ) - } - } - } - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt deleted file mode 100644 index 43668d60fb..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDragHandle -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.PaneExpansionState -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType -import androidx.compose.runtime.Composable -import androidx.compose.runtime.movableContentOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.emptyFlow -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.resources.vectorResource -import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.DeviceType -import org.meshtastic.core.navigation.TopLevelDestination -import org.meshtastic.core.ui.component.AnimatedConnectionsNavIcon -import org.meshtastic.core.ui.navigation.icon -import org.meshtastic.screenshot.marketing.resources.Res -import org.meshtastic.screenshot.marketing.resources.marketing_section_header - -/** - * The app's navigation shell, from the same [NavigationSuiteScaffold], destinations and icons as - * `MeshtasticNavigationSuite`, which itself needs the live ViewModels this generator does not have. The scaffold reads - * the window class: a compact phone gets the bottom bar, anything wider the rail with labels, and the drawer it would - * promote to at expanded widths is capped to the rail exactly as the app caps it. - */ -@Composable -internal fun AppShell(selected: TopLevelDestination, content: @Composable () -> Unit) { - val layoutType = - NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfoV2()).let { - if (it == NavigationSuiteType.NavigationDrawer) NavigationSuiteType.NavigationRail else it - } - val showLabels = layoutType == NavigationSuiteType.NavigationRail - NavigationSuiteScaffold( - layoutType = layoutType, - navigationSuiteItems = { - TopLevelDestination.entries.forEach { destination -> - item( - selected = destination == selected, - onClick = {}, - icon = { NavigationIcon(destination) }, - label = - if (showLabels) { - { Text(stringResource(destination.label)) } - } else { - null - }, - ) - } - }, - ) { - Row { content() } - } -} - -@Composable -private fun NavigationIcon(destination: TopLevelDestination) { - if (destination == TopLevelDestination.Connect) { - AnimatedConnectionsNavIcon( - connectionState = ConnectionState.Connected, - deviceType = DeviceType.fromAddress("x00:11:22:33:44:55"), - meshActivityFlow = emptyFlow(), - ) - } else { - Icon(imageVector = vectorResource(destination.icon), contentDescription = stringResource(destination.label)) - } -} - -/** Which pane a list-detail screen shows when the window has room for only one. */ -internal enum class Pane { - List, - Detail, -} - -/** - * The app's list-detail layout: `MeshtasticNavDisplay` hands the nodes and messages routes to a - * `ListDetailSceneStrategy`, which splits them into a [ListDetailPaneScaffold] with a draggable divider once - * [calculatePaneScaffoldDirective] grants a second partition - the expanded width class - and shows the one route on - * top otherwise. This is the same scaffold, directive and drag handle, with the panes given directly. - */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -internal fun ListDetail(compactPane: Pane, list: @Composable () -> Unit, detail: @Composable () -> Unit) { - val directive = calculatePaneScaffoldDirective(currentWindowAdaptiveInfoV2()) - // Movable content, as AdaptiveTwoPane does, so neither slot is emitted directly from two branches. - val listPane = remember { movableContentOf(list) } - val detailPane = remember { movableContentOf(detail) } - if (directive.maxHorizontalPartitions > 1) { - ListDetailPaneScaffold( - directive = directive, - value = - ThreePaneScaffoldValue( - primary = PaneAdaptedValue.Expanded, - secondary = PaneAdaptedValue.Expanded, - tertiary = PaneAdaptedValue.Hidden, - ), - listPane = { AnimatedPane { listPane() } }, - detailPane = { AnimatedPane { detailPane() } }, - paneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, - ) - } else { - when (compactPane) { - Pane.List -> listPane() - Pane.Detail -> detailPane() - } - } -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -private fun ThreePaneScaffoldScope.PaneExpansionDragHandle(state: PaneExpansionState) { - val interactionSource = remember { MutableInteractionSource() } - VerticalDragHandle( - modifier = - Modifier.paneExpansionDraggable( - state = state, - minTouchTargetSize = LocalMinimumInteractiveComponentSize.current, - interactionSource = interactionSource, - ), - interactionSource = interactionSource, - ) -} - -/** The conversation list's section header; the app's own is private to its feature module. */ -@Composable -internal fun SectionHeader(title: String, count: Int) { - Row( - modifier = - Modifier.fillMaxWidth() - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(Res.string.marketing_section_header, title, count), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.weight(1f), - ) - } -} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt deleted file mode 100644 index a2ea193a28..0000000000 --- a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.screenshot.marketing - -import org.jetbrains.compose.resources.StringResource -import org.meshtastic.screenshot.marketing.resources.Res -import org.meshtastic.screenshot.marketing.resources.marketing_caption_channels_description -import org.meshtastic.screenshot.marketing.resources.marketing_caption_channels_title -import org.meshtastic.screenshot.marketing.resources.marketing_caption_map_description -import org.meshtastic.screenshot.marketing.resources.marketing_caption_map_title -import org.meshtastic.screenshot.marketing.resources.marketing_caption_messages_description -import org.meshtastic.screenshot.marketing.resources.marketing_caption_messages_title -import org.meshtastic.screenshot.marketing.resources.marketing_caption_node_detail_description -import org.meshtastic.screenshot.marketing.resources.marketing_caption_node_detail_title -import org.meshtastic.screenshot.marketing.resources.marketing_caption_nodes_description -import org.meshtastic.screenshot.marketing.resources.marketing_caption_nodes_title - -/** - * Every screen the generator can draw. Which of them a surface lists, in what order and under what file name is the - * form factor's ([FormFactor.shots], [FileNaming]); [slug] is the name's stable part. The caption is the headline and - * body copy the framed variant draws above the phone, as string resources so Crowdin carries them; the two shots only - * the desktop lists have none, because no framed variant ever shows them. - */ -internal enum class Shot( - val slug: String, - val captionTitle: StringResource? = null, - val captionDescription: StringResource? = null, -) { - Messages( - "messages", - Res.string.marketing_caption_messages_title, - Res.string.marketing_caption_messages_description, - ), - Nodes("nodes", Res.string.marketing_caption_nodes_title, Res.string.marketing_caption_nodes_description), - Map("map", Res.string.marketing_caption_map_title, Res.string.marketing_caption_map_description), - NodeDetail( - "node_detail", - Res.string.marketing_caption_node_detail_title, - Res.string.marketing_caption_node_detail_description, - ), - Channels( - "channels", - Res.string.marketing_caption_channels_title, - Res.string.marketing_caption_channels_description, - ), - Connections("connections"), - Settings("settings"), -} diff --git a/schema-strings/build.gradle.kts b/schema-strings/build.gradle.kts new file mode 100644 index 0000000000..de6f3803c3 --- /dev/null +++ b/schema-strings/build.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.meshtastic.buildlogic.kotlinWarningsAsErrors + +// Writes core/resources/.../values/schema_strings.xml: every label and description in the protobufs field-metadata +// registry, keyed by schema path (`schema_lora_hop_limit`). A settings control that edits a schema field names that +// key and nothing else has to be written. `test` fails when the generated file is stale, so the schema stays the +// only place this copy lives. +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.meshtastic.detekt) + alias(libs.plugins.meshtastic.spotless) +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + vendor.set(JvmVendorSpec.JETBRAINS) + } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_25) + allWarningsAsErrors.set(kotlinWarningsAsErrors) + } +} + +dependencies { + implementation(libs.meshtastic.protobufs) + testImplementation(kotlin("test")) + testRuntimeOnly(libs.junit.platform.launcher) +} + +val repositoryRoot = isolated.rootProject.projectDirectory + +tasks.test { + useJUnitPlatform() + systemProperty("schemaStrings.rootDir", repositoryRoot.asFile.absolutePath) + inputs.dir(repositoryRoot.dir("core/resources/src/commonMain/composeResources")) +} + +tasks.register("sync") { + description = "Regenerates core/resources/.../values/schema_strings.xml from the protobufs field-metadata registry." + group = "verification" + val root = repositoryRoot + mainClass.set("org.meshtastic.schemastrings.MainKt") + classpath = sourceSets.main.get().runtimeClasspath + argumentProviders.add(CommandLineArgumentProvider { listOf(root.asFile.absolutePath) }) + outputs.upToDateWhen { false } +} diff --git a/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/EnumLabelsKt.kt b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/EnumLabelsKt.kt new file mode 100644 index 0000000000..aa6f99bb33 --- /dev/null +++ b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/EnumLabelsKt.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +/** + * The Kotlin half of the sync: a lookup from an enum value to the resource holding its schema label, so a picker shows + * the label every client shares instead of the constant's Kotlin name. + * + * The keys these build are [SchemaCatalog.keyFor]'s, so this file and `schema_strings.xml` cannot name a value + * differently. What the lookup cannot do is fail at compile time: a resource that is not there reads as null, which is + * why `SchemaEnumLabelsTest` walks every labelled constant. + */ +object EnumLabelsKt { + + /** One enum the schema labels: its proto path, the prefix its values share, and whether any value is described. */ + data class LabelledEnum(val path: String, val prefix: String, val hasDescriptions: Boolean) + + /** + * The whole generated file. Spotless reformats it afterwards, so this is the input to the formatter rather than the + * committed bytes - which is why `RepositorySyncTest` compares the branches it declares, not its text. The notice + * sits below the imports because the licence-header step deletes anything between the header and `package`. + */ + fun render(header: String, pin: String, enums: List): String = buildString { + append(header) + append("package $PACKAGE\n\n") + imports(enums).forEach { append("import ").append(it).append('\n') } + append('\n').append(notice(pin)).append('\n') + append(LABEL_DOC) + append("@Suppress(\"CyclomaticComplexMethod\")\n") + append("fun Enum<*>.schemaLabelRes(): StringResource? = when (this) {\n") + enums.forEach { append(branch(it, "schemaLabel")) } + append(" else -> null\n}\n") + append(DESCRIPTION_DOC) + append("@Suppress(\"CyclomaticComplexMethod\")\n") + append("fun Enum<*>.schemaDescriptionRes(): StringResource? = when (this) {\n") + enums.filter { it.hasDescriptions }.forEach { append(branch(it, "schemaDescription")) } + append(" else -> null\n}\n") + append(PREFIXES_DOC) + append("val $PREFIXES: Set =\n setOf(\n") + enums.forEach { append(" \"").append(it.prefix).append("\",\n") } + append(" )\n") + append(HELPER) + } + + /** + * One `when` branch. A branch that would not fit on a line is an error rather than a wrap: a wrapped branch makes + * the formatter space out every other branch in the same `when`, and the fix belongs in the name, not here. + */ + private fun branch(enum: LabelledEnum, function: String): String { + val line = " is ${enum.path} -> $function(\"${enum.prefix}\")" + require(line.length <= MAX_LINE) { "branch for ${enum.path} is ${line.length} characters, over $MAX_LINE" } + return "$line\n" + } + + /** The name of the generated set, so the test and the renderer cannot disagree about it. */ + const val PREFIXES = "schemaEnumValuePrefixes" + + /** `"schema_lora_modempreset_",` as the committed file spells one entry of that set. */ + val prefixEntryPattern: Regex = Regex("""^\s{8}"(schema_[a-z0-9_]+_)",$""", RegexOption.MULTILINE) + + /** `is Config.DeviceConfig.Role -> ...`, as the committed file spells it, for the drift check. */ + val branchPattern: Regex = + Regex("""^\s*is ([\w.]+) -> schema(Label|Description)\("([^"]+)"\)""", RegexOption.MULTILINE) + + /** `Config.DeviceConfig.Role` needs only `Config` imported; a top-level enum needs itself. */ + private fun imports(enums: List): List = + (FIXED_IMPORTS + enums.map { "$PROTO_PACKAGE.${it.path.substringBefore('.')}" }).distinct().sorted() + + fun notice(pin: String): String = + "// Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs $pin.\n" + + "// Do not edit: change the schema, then run sync." + + private const val MAX_LINE = 120 + private const val PACKAGE = "org.meshtastic.core.model" + private const val PROTO_PACKAGE = "org.meshtastic.proto" + + private val FIXED_IMPORTS = + listOf( + "org.jetbrains.compose.resources.StringResource", + "org.meshtastic.core.resources.Res", + "org.meshtastic.core.resources.allStringResources", + ) + + private val LABEL_DOC = + """ + | + |/** + | * The schema's label for this enum value, or null where the schema does not name it. A picker shows this in + | * place of the constant's Kotlin name, and falls back to that name when it is null, so a value the schema has + | * not reached still renders. + | */ + |""" + .trimMargin() + + private val PREFIXES_DOC = + """ + | + |/** + | * The resource prefix every value of a labelled enum shares, one per enum. A consumer that has to tell an enum + | * value's resource from a field's - settings search, which indexes fields and not the values they offer - reads + | * this rather than keeping its own copy of the list. + | */ + |""" + .trimMargin() + + private val DESCRIPTION_DOC = + """ + | + |/** The schema's one-sentence explanation of this enum value, or null where it has none. */ + |""" + .trimMargin() + + private val HELPER = + """ + | + |private fun Enum<*>.schemaLabel(prefix: String): StringResource? = + | Res.allStringResources[prefix + name.lowercase()] + | + |private fun Enum<*>.schemaDescription(prefix: String): StringResource? = + | Res.allStringResources[prefix + name.lowercase() + "_description"] + |""" + .trimMargin() +} diff --git a/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/Main.kt b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/Main.kt new file mode 100644 index 0000000000..b713d35c87 --- /dev/null +++ b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/Main.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +import java.io.File + +/** `sync `: write the English schema strings from the registry on the classpath. */ +fun main(args: Array) { + val root = args.firstOrNull()?.let(::File) ?: error("usage: sync ") + println(SchemaStringsSync(root).apply()) +} diff --git a/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaCatalog.kt b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaCatalog.kt new file mode 100644 index 0000000000..d7bf4c0fb7 --- /dev/null +++ b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaCatalog.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +import com.squareup.wire.WireEnum +import com.squareup.wire.WireField +import org.meshtastic.proto.FieldMetadata +import org.meshtastic.proto.FieldMetadataRegistry +import java.io.File +import java.util.SortedMap +import java.util.TreeMap +import java.util.zip.ZipFile + +/** + * Every label and description the field-metadata registry carries, keyed by a name derived from the schema path, so the + * code that shows a field names the resource and nothing else has to. + * + * `Config.LoRaConfig.hop_limit` becomes `schema_lora_hop_limit`; its description `schema_lora_hop_limit_description`; + * the enum value `Config.PositionConfig.PositionFlags.DOP` becomes `schema_position_positionflags_dop`. + */ +object SchemaCatalog { + private const val PROTO_PACKAGE = "meshtastic." + private const val GENERATED_PACKAGE = "org.meshtastic.proto." + private const val PREFIX = "schema_" + private const val DESCRIPTION_SUFFIX = "_description" + private val topLevelContainers = setOf("Config", "ModuleConfig") + + /** `resource -> English`, sorted by resource, for every annotated field and enum value on the classpath. */ + fun all(): SortedMap { + val out = TreeMap() + val origins = HashMap() + fun put(resource: String, text: String?, origin: String) { + if (text.isNullOrBlank()) return + val earlier = origins.put(resource, origin) + check(earlier == null) { "$origin and $earlier both derive the resource '$resource'" } + out[resource] = text + } + for (path in generatedTypePaths()) { + val type = Class.forName(GENERATED_PACKAGE + path.replace('.', '$')) + val constants = type.enumConstants + if (constants != null) { + constants.filterIsInstance().forEach { constant -> + val meta = FieldMetadataRegistry.forEnumValue(PROTO_PACKAGE + path, constant.value) + val name = (constant as Enum<*>).name + put(keyFor(path, name), meta?.label, "$path.$name.label") + put(keyFor(path, name) + DESCRIPTION_SUFFIX, meta?.description, "$path.$name.description") + } + } else { + type.declaredFields.forEach { property -> + val tag = property.getAnnotation(WireField::class.java)?.tag ?: return@forEach + val meta: FieldMetadata? = FieldMetadataRegistry.get(PROTO_PACKAGE + path, tag) + val key = keyFor(path, property.name) + val origin = "$path.${property.name}" + put(key, meta?.label, "$origin.label") + put(key + DESCRIPTION_SUFFIX, meta?.description, "$origin.description") + } + } + } + return out + } + + /** + * Every distinct unit symbol the schema declares on a field. The app names these in `FieldMetadataUnits.kt`, which + * renders nothing for a symbol it does not know, so a new one has to be added there deliberately. + */ + fun units(): Set { + val out = sortedSetOf() + for (path in generatedTypePaths()) { + val type = Class.forName(GENERATED_PACKAGE + path.replace('.', '$')) + if (type.enumConstants != null) continue + type.declaredFields.forEach { property -> + val tag = property.getAnnotation(WireField::class.java)?.tag ?: return@forEach + val unit = FieldMetadataRegistry.get(PROTO_PACKAGE + path, tag)?.unit + if (!unit.isNullOrBlank()) out += unit + } + } + return out + } + + /** Every enum the schema labels, in proto-path order, for the Kotlin accessors that read those labels. */ + fun labelledEnums(): List = generatedTypePaths().mapNotNull { path -> + val type = Class.forName(GENERATED_PACKAGE + path.replace('.', '$')) + val constants = type.enumConstants ?: return@mapNotNull null + val metadata = + constants.filterIsInstance().mapNotNull { + FieldMetadataRegistry.forEnumValue(PROTO_PACKAGE + path, it.value) + } + if (metadata.none { !it.label.isNullOrBlank() }) return@mapNotNull null + EnumLabelsKt.LabelledEnum( + path = path, + prefix = keyFor(path, ""), + hasDescriptions = metadata.any { !it.description.isNullOrBlank() }, + ) + } + + /** + * The resource for a field or enum value. Each message segment is lowercased with a trailing `Config` dropped, and + * the `Config`/`ModuleConfig` container is dropped: `ModuleConfig.MQTTConfig.address` is `schema_mqtt_address`. + */ + fun keyFor(path: String, member: String): String { + val segments = path.split('.').filterIndexed { index, s -> !(index == 0 && s in topLevelContainers) } + val message = segments.joinToString("_") { it.removeSuffix("Config").ifEmpty { it }.lowercase() } + return PREFIX + message + "_" + member.lowercase() + } + + /** Proto paths (`Config.PositionConfig`) of every generated type in the protobufs jar, from its class list. */ + private fun generatedTypePaths(): List { + val jar = File(FieldMetadataRegistry::class.java.protectionDomain.codeSource.location.toURI()) + val prefix = GENERATED_PACKAGE.replace('.', '/') + return ZipFile(jar).use { zip -> + zip.entries() + .asSequence() + .map { it.name } + .filter { it.startsWith(prefix) && it.endsWith(".class") && '/' !in it.removePrefix(prefix) } + .map { it.removePrefix(prefix).removeSuffix(".class") } + .filter { name -> name.split('$').all { it.isNotEmpty() && it[0].isUpperCase() } } + .map { it.replace('$', '.') } + .sorted() + .toList() + } + } +} diff --git a/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaStringsSync.kt b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaStringsSync.kt new file mode 100644 index 0000000000..da85db7be6 --- /dev/null +++ b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/SchemaStringsSync.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +import java.io.File +import java.time.Year + +/** The repository view of the sync: where the files live, what the English file should contain, how to write it. */ +class SchemaStringsSync(private val rootDir: File) { + private val valuesRoot = rootDir.resolve("core/resources/src/commonMain/composeResources") + private val catalog = rootDir.resolve("gradle/libs.versions.toml") + val englishStrings: File = valuesRoot.resolve("values/$HAND_WRITTEN") + val englishSchemaStrings: File = valuesRoot.resolve("values/$GENERATED") + val enumLabels: File = rootDir.resolve("core/model/src/commonMain/kotlin/$GENERATED_KT") + + /** Every hand-written strings file: the English one and one per locale. */ + val handWrittenFiles: List + get() = + listOf(englishStrings) + + valuesRoot + .listFiles { f -> f.isDirectory && f.name.startsWith("values-") && f.resolve(HAND_WRITTEN).isFile } + .orEmpty() + .sortedBy { it.name } + .map { it.resolve(HAND_WRITTEN) } + + /** The `org.meshtastic:protobufs` version the build resolves, from the version catalog. */ + val catalogPin: String by lazy { + catalogPinLine.find(catalog.readText())?.groupValues?.get(1) ?: error("no meshtastic-protobufs pin in $catalog") + } + + /** The pin the committed generated file says it was built from, or null when the file is absent or unmarked. */ + val recordedPin: String? + get() = englishSchemaStrings.takeIf { it.isFile }?.let { recordedPin(it.readText()) } + + /** The English file the registry implies. The header is borrowed from `strings.xml`, so the licence is one text. */ + fun expectedEnglish(): String = StringsXml.render( + header = StringsXml.header(englishStrings.readText()), + bodies = SchemaCatalog.all().mapValues { (_, text) -> StringsXml.escape(text) }, + notice = notice(catalogPin), + ) + + /** + * The Kotlin accessors the registry implies. The licence header is the committed file's own where there is one, so + * a rerun does not restamp a year Spotless is happy with - everything before `package` is that header. + */ + fun expectedEnumLabels(): String = EnumLabelsKt.render( + header = enumLabels.takeIf { it.isFile }?.let { licenceHeader(it.readText()) } ?: freshLicenceHeader(), + pin = catalogPin, + enums = SchemaCatalog.labelledEnums(), + ) + + /** The pin the committed Kotlin file says it was built from, or null when it is absent or unmarked. */ + val recordedEnumLabelsPin: String? + get() = enumLabels.takeIf { it.isFile }?.let { noticePin.find(it.readText())?.groupValues?.get(1) } + + private fun licenceHeader(kotlin: String): String = kotlin.substringBefore("package ").substringBefore("// ") + + private fun freshLicenceHeader(): String = + rootDir.resolve("config/spotless/copyright.kt").readText().replace("\$YEAR", YEAR) + "\n" + + /** Schema-named keys that someone wrote into a hand-written file, by file. Those files must never carry one. */ + fun strays(): Map> = handWrittenFiles + .associateWith { file -> schemaKeys(StringsXml.bodies(file.readText()).keys) } + .filterValues { it.isNotEmpty() } + + fun apply(): String { + val text = expectedEnglish() + englishSchemaStrings.writeText(text) + enumLabels.writeText(expectedEnumLabels()) + return "wrote ${StringsXml.bodies(text).size} strings and " + + "${SchemaCatalog.labelledEnums().size} enum accessors from protobufs $catalogPin" + } + + private fun schemaKeys(names: Set): Set = names.filterTo(HashSet()) { it.startsWith("schema_") } + + companion object { + const val HAND_WRITTEN = "strings.xml" + const val GENERATED = "schema_strings.xml" + const val GENERATED_KT = "org/meshtastic/core/model/SchemaEnumLabels.kt" + private val YEAR = Year.now().toString() + + private val catalogPinLine = Regex("""^meshtastic-protobufs = "([^"]+)"$""", RegexOption.MULTILINE) + private val noticePin = Regex("""from org\.meshtastic:protobufs (\S+)\.""") + + /** The first line inside ``: says where the file comes from and which pin it reflects. */ + fun notice(pin: String): String = + "" + + fun recordedPin(xml: String): String? = noticePin.find(xml)?.groupValues?.get(1) + } +} diff --git a/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/StringsXml.kt b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/StringsXml.kt new file mode 100644 index 0000000000..60ab692d1c --- /dev/null +++ b/schema-strings/src/main/kotlin/org/meshtastic/schemastrings/StringsXml.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +/** + * Line-level edits of a Compose `strings.xml`. Element bodies are copied verbatim so translations keep their escapes. + */ +object StringsXml { + private val element = + Regex("""[ \t]*]*>(.*?)[ \t]*\r?\n?""", RegexOption.DOT_MATCHES_ALL) + + /** Every `` body by name, in file order. */ + fun bodies(xml: String): LinkedHashMap = + element.findAll(xml).associateTo(LinkedHashMap()) { it.groupValues[1] to it.groupValues[2] } + + /** The text before ``, which carries the licence header. */ + fun header(xml: String): String = xml.substringBefore("") + + /** A whole file: header, an optional notice, then one element per entry in the map's (sorted) order. */ + fun render(header: String, bodies: Map, notice: String?): String = buildString { + append(header) + append("\n") + notice?.let { append(" ").append(it).append('\n') } + for ((name, body) in bodies) { + append(" ").append(body).append("\n") + } + append("\n") + } + + /** Only the XML metacharacters; quotes stay bare, as `check-string-escapes.py` requires. */ + fun escape(text: String): String = text.replace("&", "&").replace("<", "<").replace(">", ">") +} diff --git a/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/RepositorySyncTest.kt b/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/RepositorySyncTest.kt new file mode 100644 index 0000000000..44403ed23a --- /dev/null +++ b/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/RepositorySyncTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * The committed generated file is checked against the registry only while it records the pin the build resolves. A + * merged protobufs bump leaves the two apart until the scheduled-updates run it triggers re-syncs; that window is not + * an error, a hand edit at a matching pin is. + */ +class RepositorySyncTest { + + private val sync = SchemaStringsSync(File(System.getProperty("schemaStrings.rootDir"))) + + @Test + fun `the generated file records the pin it was built from`() { + assertNotNull(sync.recordedPin, "values/schema_strings.xml carries no pin: run ./gradlew :schema-strings:sync") + } + + @Test + fun `at a matching pin the generated file is what the registry implies`() { + if (sync.recordedPin != sync.catalogPin) { + println( + "protobufs moved from ${sync.recordedPin} to ${sync.catalogPin}; scheduled-updates re-syncs the file", + ) + return + } + assertEquals( + sync.expectedEnglish(), + sync.englishSchemaStrings.readText(), + "core/resources values/schema_strings.xml is stale: run ./gradlew :schema-strings:sync", + ) + } + + @Test + fun `the generated Kotlin records the pin it was built from`() { + assertNotNull( + sync.recordedEnumLabelsPin, + "core/model SchemaEnumLabels.kt carries no pin: run ./gradlew :schema-strings:sync", + ) + } + + /** + * Compares the accessors the file declares rather than its bytes: Spotless reformats what the sync writes, so its + * text is the formatter's and only the set of enums and prefixes is the sync's to promise. + */ + @Test + fun `at a matching pin the generated Kotlin declares the accessors the registry implies`() { + if (sync.recordedEnumLabelsPin != sync.catalogPin) { + println("protobufs moved to ${sync.catalogPin}; scheduled-updates re-syncs SchemaEnumLabels.kt") + return + } + val committed = + EnumLabelsKt.branchPattern + .findAll(sync.enumLabels.readText()) + .map { Triple(it.groupValues[1], it.groupValues[2], it.groupValues[3]) } + .toSet() + val expected = + SchemaCatalog.labelledEnums() + .flatMap { enum -> + listOfNotNull( + Triple(enum.path, "Label", enum.prefix), + Triple(enum.path, "Description", enum.prefix).takeIf { enum.hasDescriptions }, + ) + } + .toSet() + + assertEquals(expected, committed, "core/model SchemaEnumLabels.kt is stale: run ./gradlew :schema-strings:sync") + } + + @Test + fun `the generated set names the prefix of every labelled enum`() { + if (sync.recordedEnumLabelsPin != sync.catalogPin) { + println("protobufs moved to ${sync.catalogPin}; scheduled-updates re-syncs SchemaEnumLabels.kt") + return + } + val declared = + EnumLabelsKt.prefixEntryPattern.findAll(sync.enumLabels.readText()).map { it.groupValues[1] }.toSet() + + assertEquals( + SchemaCatalog.labelledEnums().map { it.prefix }.toSet(), + declared, + "${EnumLabelsKt.PREFIXES} in SchemaEnumLabels.kt is stale: run ./gradlew :schema-strings:sync", + ) + } + + @Test + fun `every labelled enum builds a prefix the generated strings carry`() { + val strings = StringsXml.bodies(sync.englishSchemaStrings.readText()).keys + val missing = SchemaCatalog.labelledEnums().filter { enum -> strings.none { it.startsWith(enum.prefix) } } + + assertTrue( + missing.isEmpty(), + "these enums build a resource prefix nothing in schema_strings.xml matches: " + + missing.joinToString { "${it.path} (${it.prefix})" }, + ) + } + + @Test + fun `no schema key is written by hand`() { + val strays = sync.strays() + + assertTrue( + strays.isEmpty(), + strays.entries.joinToString( + "\n", + prefix = "schema_ keys are generated into schema_strings.xml only; remove them from:\n", + ) { (file, keys) -> + "${file.parentFile.name}/${file.name}: ${keys.sorted().joinToString()}" + }, + ) + } +} diff --git a/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/SchemaStringsTest.kt b/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/SchemaStringsTest.kt new file mode 100644 index 0000000000..7a6da9cb6a --- /dev/null +++ b/schema-strings/src/test/kotlin/org/meshtastic/schemastrings/SchemaStringsTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.schemastrings + +import org.meshtastic.proto.Config +import org.meshtastic.proto.hop_limit +import org.meshtastic.proto.position_flags +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SchemaStringsTest { + + private val catalog by lazy { SchemaCatalog.all() } + + @Test + fun `a key drops the container and the Config suffix and lowercases the rest`() { + assertEquals("schema_lora_hop_limit", SchemaCatalog.keyFor("Config.LoRaConfig", "hop_limit")) + assertEquals("schema_mqtt_address", SchemaCatalog.keyFor("ModuleConfig.MQTTConfig", "address")) + assertEquals("schema_network_ipv4_ip", SchemaCatalog.keyFor("Config.NetworkConfig.IpV4Config", "ip")) + assertEquals( + "schema_position_positionflags_dop", + SchemaCatalog.keyFor("Config.PositionConfig.PositionFlags", "DOP"), + ) + assertEquals("schema_team_white", SchemaCatalog.keyFor("Team", "White")) + } + + @Test + fun `a field's label and description resolve to what the companion accessor returns`() { + assertEquals(Config.PositionConfig.position_flags.label, catalog["schema_position_position_flags"]) + assertEquals(Config.LoRaConfig.hop_limit.description, catalog["schema_lora_hop_limit_description"]) + } + + @Test + fun `an enum value with a label is emitted and one without is not`() { + assertEquals("DOP", catalog["schema_position_positionflags_dop"]) + assertFalse("schema_position_positionflags_unset" in catalog, "UNSET carries no label") + } + + @Test + fun `an unannotated field is absent rather than blank`() { + assertFalse(catalog.keys.any { it == "schema_device_serial_enabled" }) + assertTrue(catalog.values.none { it.isBlank() }) + } + + @Test + fun `the catalogue is large enough to be the whole registry`() { + assertTrue(catalog.size > 400, "only ${catalog.size} strings: the class walk missed generated types") + } + + @Test + fun `the schema declares only the unit symbols the app can name`() { + assertEquals(setOf("dBm", "kHz", "m", "ms", "s"), SchemaCatalog.units()) + } + + @Test + fun `the notice carries the pin and reads back`() { + val pin = "2.8.0.111-g45f6b7e-SNAPSHOT" + val xml = StringsXml.render("", emptyMap(), notice = SchemaStringsSync.notice(pin)) + + assertEquals(pin, SchemaStringsSync.recordedPin(xml)) + assertEquals(null, SchemaStringsSync.recordedPin("\n\n")) + } + + @Test + fun `rendering escapes markup and leaves quotes bare`() { + val xml = StringsXml.render("\n", mapOf("a" to StringsXml.escape("Tom's & \"c\"")), notice = null) + + assertEquals( + "\n\n Tom's <b> & \"c\"\n\n", + xml, + ) + } + + @Test + fun `reading bodies keeps a multi-line element whole`() { + val xml = + "\n k\n multi\n line\n\n" + + assertEquals(mapOf("keep" to "k", "go" to "multi\n line"), StringsXml.bodies(xml)) + } +} diff --git a/screenshot-tests/docs-screenshot-aliases.properties b/screenshot-tests/docs-screenshot-aliases.properties index f76329cfc0..1d4905478e 100644 --- a/screenshot-tests/docs-screenshot-aliases.properties +++ b/screenshot-tests/docs-screenshot-aliases.properties @@ -15,7 +15,6 @@ connections_bluetooth_scan.png=ScreenshotConnectionsBluetoothScan_Light_b29dc7a7 connections_transport_filters.png=ScreenshotTransportSelector_Light_b29dc7a7_0.png connections_connecting.png=ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png connections_disconnect.png=ScreenshotDisconnectButton_Light_b29dc7a7_0.png -connections_empty_state.png=ScreenshotEmptyStateContent_Light_b29dc7a7_0.png # Firmware firmware_checking.png=ScreenshotFirmwareChecking_Light_b29dc7a7_0.png diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt index f4d5198d00..6587df4916 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt @@ -20,9 +20,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark import com.android.tools.screenshot.PreviewTest import org.meshtastic.feature.connections.component.ConnectingDeviceInfoPreview +import org.meshtastic.feature.connections.component.DemoModeSectionPreview import org.meshtastic.feature.connections.component.DeviceListItemPreview import org.meshtastic.feature.connections.component.DeviceSectionHeaderPreview import org.meshtastic.feature.connections.component.DisconnectButtonPreview +import org.meshtastic.feature.connections.component.TransportSelectorNoBluetoothPreview +import org.meshtastic.feature.connections.component.TransportSelectorNoUsbPreview import org.meshtastic.feature.connections.component.TransportSelectorPreview @PreviewTest @@ -59,3 +62,24 @@ fun ScreenshotDeviceSectionHeader() { fun ScreenshotTransportSelector() { TransportSelectorPreview() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotTransportSelectorNoBluetooth() { + TransportSelectorNoBluetoothPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotTransportSelectorNoUsb() { + TransportSelectorNoUsbPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotDemoModeSection() { + DemoModeSectionPreview() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt index 4677aa641a..d8633b30fb 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt @@ -29,6 +29,9 @@ import org.meshtastic.feature.node.component.HopHistogramContentPreview import org.meshtastic.feature.node.component.HopHistogramEmptyPreview import org.meshtastic.feature.node.component.NodeDetailsSectionPreview import org.meshtastic.feature.node.component.NodeDetailsSectionSignedPreview +import org.meshtastic.feature.node.component.NodeDetailsSectionWithMakerDeviceHeroPreview +import org.meshtastic.feature.node.component.NodeFilterSearchBarEmptyPreview +import org.meshtastic.feature.node.component.NodeFilterSearchBarWithQueryPreview import org.meshtastic.feature.node.component.NodeItemCompactActivePreview import org.meshtastic.feature.node.component.NodeItemCompactAllFieldsPreview import org.meshtastic.feature.node.component.NodeItemCompactMinimalPreview @@ -151,6 +154,13 @@ fun ScreenshotNodeDetailsSectionSigned() { NodeDetailsSectionSignedPreview() } +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotNodeDetailsSectionMakerHero() { + NodeDetailsSectionWithMakerDeviceHeroPreview() +} + @PreviewTest @PreviewLightDark @Composable @@ -351,3 +361,17 @@ fun ScreenshotEnvironmentMetricsContentLightning() { fun ScreenshotAirQualityCardsStatus() { PreviewAirQualityCardsStatus() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotNodeFilterSearchBarEmpty() { + NodeFilterSearchBarEmptyPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotNodeFilterSearchBarWithQuery() { + NodeFilterSearchBarWithQueryPreview() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt index 6c465a2e38..06e1ce9087 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt @@ -49,6 +49,8 @@ import org.meshtastic.feature.settings.radio.component.TakServerSectionFailedPre import org.meshtastic.feature.settings.radio.component.TakTestCardIdlePreview import org.meshtastic.feature.settings.radio.component.TakTestCardResultsPreview import org.meshtastic.feature.settings.radio.component.TakTestCardRunningPreview +import org.meshtastic.feature.settings.search.SettingsSearchNoResultsPreview +import org.meshtastic.feature.settings.search.SettingsSearchResultsPreview @PreviewTest @PreviewLightDark @@ -267,3 +269,17 @@ fun ScreenshotSampleNodeCompleteToggleMatrix() { fun ScreenshotAppFunctionsSettings() { PreviewAppFunctionsSettings() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotSettingsSearchResults() { + SettingsSearchResultsPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotSettingsSearchNoResults() { + SettingsSearchNoResultsPreview() +} diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..ecb8cfdbb6 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..664f726843 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDemoModeSection_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..229f93ee1e Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..eb36ac651e Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoBluetooth_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..c6d86c866e Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..4f4d91d2bf Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportSelectorNoUsb_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Dark_d19fbf1f_0.png index 53f251326b..edd687a221 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Light_b29dc7a7_0.png index 42e1c1713b..ac83ae1b36 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageItemMarkdown_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..9595a3d2dd Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..dd68a2527d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionMakerHero_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..458b6c8d3a Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..1529b8ab4d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarEmpty_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..d65c0a1bab Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..83dfec8c94 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeFilterSearchBarWithQuery_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png index 92c9412c83..25a3c11188 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png index 9de9164ed3..1fd0e56673 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPacketAuthenticityDefault_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..5ae8ff5fd0 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..6a58c04696 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchNoResults_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..ba66101340 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..b22a171e5b Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSettingsSearchResults_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Dark_d19fbf1f_0.png index a740fcdfe4..a2e2ad91d4 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Light_b29dc7a7_0.png index bfa18e3801..81ab288461 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakConfigCard_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png index 3365e43a4b..db480baf1b 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png index c06ea1127c..14c9cc4597 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png differ diff --git a/scripts/bump-version-name.py b/scripts/bump-version-name.py new file mode 100755 index 0000000000..c5abd8a3e8 --- /dev/null +++ b/scripts/bump-version-name.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Open the next version line: VERSION_NAME_BASE plus everything pull-request.yml requires with it. + +That is the AppStream entry in metainfo.xml, its five URLs moved to the +new version's release assets, and the Play what's-new rendered from the entry by +sync-play-changelog.py. The entry's paragraph is a placeholder; rewrite it and re-run +sync-play-changelog.py before the internal cut, or it ships as the store text. + +Running it twice for the same version changes nothing the second time. + + python3 scripts/bump-version-name.py +""" +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +CONFIG = REPO_ROOT / "config.properties" +METAINFO = REPO_ROOT / "desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml" +SYNC = REPO_ROOT / "scripts/sync-play-changelog.py" +PLACEHOLDER = "Stability and reliability fixes." + + +def bump_config(v: str) -> None: + text, n = re.subn(r"^VERSION_NAME_BASE=.*$", f"VERSION_NAME_BASE={v}", CONFIG.read_text(), flags=re.M) + if n != 1: + sys.exit(f"expected one VERSION_NAME_BASE line in {CONFIG.name}, found {n}") + CONFIG.write_text(text) + + +def bump_metainfo(v: str) -> None: + text = METAINFO.read_text() + if f' entry in {METAINFO.name} to insert above") + ind = first.group(1) + date = datetime.now(timezone.utc).date().isoformat() + entry = ( + f'{ind}\n' + f"{ind} \n" + f"{ind}

{PLACEHOLDER}

\n" + f"{ind}
\n" + f"{ind}
\n" + ) + text = text[: first.start()] + entry + text[first.start() :] + text = re.sub(r"([^<]*/releases/download/)v[^/<]+/", rf"\g<1>v{v}/", text) + METAINFO.write_text(text) + + +def main() -> int: + if len(sys.argv) != 2 or not re.fullmatch(r"\d+\.\d+\.\d+", sys.argv[1]): + sys.exit("usage: bump-version-name.py ") + v = sys.argv[1] + bump_config(v) + bump_metainfo(v) + subprocess.run([sys.executable, str(SYNC)], check=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-changes-filter.py b/scripts/check-changes-filter.py new file mode 100755 index 0000000000..99a17bd482 --- /dev/null +++ b/scripts/check-changes-filter.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Check that every module root has an entry in pull-request.yml's android filter. + +Each top-level directory holding a module in settings.gradle.kts needs a '/**' +line in the android filter, or a PR that touches only that module skips CI. Entries +in the other filters do not count. Exits non-zero on drift. +""" + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Filter roots that are intentionally not Gradle module roots +# (CI/workflow implementation + shared build infrastructure). +ALLOWED_INFRA_ROOTS = {'.github', 'build-logic', 'config', 'gradle'} +ALLOWED_EXTRA_ROOTS = {'baselineprofile'} + + +def android_filter(workflow: str) -> str: + """Return the entries of the android filter, which is what gates validate-and-build.""" + lines = workflow.split('\n') + try: + filters = next(i for i, line in enumerate(lines) if line.strip() == 'filters: |') + start = next(i for i in range(filters + 1, len(lines)) if lines[i].strip() == 'android:') + except StopIteration: + raise SystemExit('check-changes filter drift detected: no android filter in pull-request.yml') + indent = len(lines[start]) - len(lines[start].lstrip()) + end = start + 1 + while end < len(lines): + line = lines[end] + if line.strip() and len(line) - len(line.lstrip()) <= indent: + break + end += 1 + return '\n'.join(lines[start + 1:end]) + + +def main() -> None: + settings = (REPO_ROOT / 'settings.gradle.kts').read_text() + workflow = (REPO_ROOT / '.github/workflows/pull-request.yml').read_text() + + module_roots = { + module.split(':')[0] + for module in re.findall(r'":([^"]+)"', settings) + } + expected_roots = module_roots | ALLOWED_EXTRA_ROOTS + + # Only a whole-root entry covers a root: 'core/ble/**' leaves the rest of core/ unfiltered. + filter_paths = set(re.findall(r"-\s*'([^'/]+)/\*\*'", android_filter(workflow))) + + missing = sorted(expected_roots - filter_paths) + unexpected = sorted(filter_paths - expected_roots - ALLOWED_INFRA_ROOTS) + + if missing or unexpected: + print('check-changes filter drift detected:') + if missing: + print(' Missing roots:', ', '.join(missing)) + if unexpected: + print(' Unexpected roots:', ', '.join(unexpected)) + raise SystemExit(1) + + print('check-changes filter is aligned with settings.gradle module roots.') + + +if __name__ == '__main__': + main() diff --git a/scripts/check-module-list.py b/scripts/check-module-list.py new file mode 100755 index 0000000000..096f234fe3 --- /dev/null +++ b/scripts/check-module-list.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Check ALL_MODULES_FULL in RootConventionPlugin.kt against settings.gradle.kts. + +The list is a hand-maintained copy of the settings includes, because iterating +subprojects {} is incompatible with Isolated Projects. A module missing from it is +absent from Dokka aggregation, Kover aggregation and kmpSmokeCompile. Exits non-zero +on drift. +""" + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Test harnesses and generators kept out of root aggregation. Exempt in both +# directions: the guard neither forces them in nor lets them be added back. +EXEMPT = { + ':baselineprofile', + ':core:konsist', + ':docs-screenshots', + ':schema-strings', + ':screenshot-tests', + ':store-screenshots', +} + + +def main() -> None: + settings = (REPO_ROOT / 'settings.gradle.kts').read_text() + plugin = ( + REPO_ROOT / 'build-logic/convention/src/main/kotlin/RootConventionPlugin.kt' + ).read_text() + + include = settings[settings.index('include('):] + modules = set(re.findall(r'"(:[^"]+)"', include[: include.index('\n)')])) + + # ALL_MODULES_FULL appears twice (declaration and use), so bound the slice to + # the declaration's own closing paren rather than searching for the name. + decl = plugin[plugin.index('ALL_MODULES_FULL ='):] + listed = set(re.findall(r'"(:[^"]+)"', decl[: decl.index('\n )')])) + + missing = sorted(modules - listed - EXEMPT) + extra = sorted(listed - modules) + readded = sorted(listed & EXEMPT) + + problems = [] + for m in missing: + problems.append( + f'{m} is in settings.gradle.kts but not ALL_MODULES_FULL -- it is absent ' + 'from Dokka/Kover aggregation and kmpSmokeCompile' + ) + for m in extra: + problems.append(f'{m} is in ALL_MODULES_FULL but no longer in settings.gradle.kts') + for m in readded: + problems.append( + f'{m} is exempt from root aggregation (#6412) but present in ' + 'ALL_MODULES_FULL -- remove it, or drop it from the exempt set here' + ) + + if problems: + print('Root module list drift detected:') + for p in problems: + print(' -', p) + raise SystemExit(1) + + print(f'{len(listed)} modules verified against settings.gradle.kts ' + f'({len(EXEMPT)} exempt).') + + +if __name__ == '__main__': + main() diff --git a/scripts/check-test-shards.py b/scripts/check-test-shards.py new file mode 100755 index 0000000000..d4b8257d0d --- /dev/null +++ b/scripts/check-test-shards.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check that every module with tests is wired into a reusable-check.yml test shard. + +The shard task lists are hand-maintained. Every module in settings.gradle.kts must +have a test task in the shard matrix or be exempted here, and an exempt test-less +module that gains test sources fails until it is wired into a shard. Exits non-zero +on drift. +""" + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Modules whose tests run in a dedicated job or only on-device -- +# exempt unconditionally. +COVERED_ELSEWHERE = { + ':screenshot-tests', # dedicated screenshot-check job + ':docs-screenshots', # doc-screenshot generation (screenshot tooling) + ':baselineprofile', # benchmark module, instrumented-only + ':store-screenshots', # store-listing screenshots from the real app, instrumented-only +} +# Modules with no unit-test sources yet. One of these gaining test +# sources fails the guard: move it into a shard in reusable-check.yml +# and remove it from this list. +NO_TESTS_YET = { + ':core:di', + ':core:nfc', + ':core:resources', +} + + +def has_test_sources(module: str) -> bool: + root = REPO_ROOT / module.lstrip(':').replace(':', '/') + return any( + f.suffix == '.kt' + for d in root.glob('src/*') + if 'test' in d.name.lower() + for f in d.rglob('*.kt') + ) + + +def main() -> None: + settings = (REPO_ROOT / 'settings.gradle.kts').read_text() + check = (REPO_ROOT / '.github/workflows/reusable-check.yml').read_text() + + modules = set(re.findall(r'"(:[^"]+)"', settings)) + + shards = check.split('# ── Sharded Unit Tests')[1].split('# ── Android Build')[0] + # A commented-out task runs nothing, so it must not count as coverage. + shards = '\n'.join(re.sub(r'(^|\s)#.*$', '', line) for line in shards.splitlines()) + + problems = [] + for m in sorted(modules): + if m in COVERED_ELSEWHERE: + continue + if m in NO_TESTS_YET: + if has_test_sources(m): + problems.append(f'{m} is exempt as test-less but has test sources -- wire it into a shard') + # Require an actual test task (allTests / test / testUnitTest), + # not just any reference -- a lone kover entry must not satisfy this. + elif not re.search(rf'{re.escape(m)}:(allTests|test)', shards): + problems.append(f'{m} has no test task in any reusable-check.yml test shard') + + if problems: + print('CI shard coverage drift detected:') + for p in problems: + print(' -', p) + raise SystemExit(1) + exempt = COVERED_ELSEWHERE | NO_TESTS_YET + print(f'{len(modules) - len(modules & exempt)} modules verified against the shard matrix.') + + +if __name__ == '__main__': + main() diff --git a/scripts/docs/regenerate-versions.py b/scripts/docs/regenerate-versions.py index 4c69637e20..59b01b0133 100755 --- a/scripts/docs/regenerate-versions.py +++ b/scripts/docs/regenerate-versions.py @@ -7,14 +7,15 @@ never disagree with what is published. Channel layout on gh-pages: / production release docs (owns the root) - /vX.Y.Z/ permanent per-release snapshot + /vX.Y.Z/ per-release snapshot (current and previous release) /vX.Y.Z-open.N/ per-tag open-testing snapshot /vX.Y.Z-closed.N/ per-tag closed-testing snapshot /main/ snapshot of the main branch /api/ Dokka reference (unversioned) Prerelease snapshots accumulate during a version cycle and are reaped by -post-release-cleanup.yml once the production vX.Y.Z tag ships. +post-release-cleanup.yml once the production vX.Y.Z tag ships. The same +cleanup removes every production snapshot older than the one before vX.Y.Z. Usage: regenerate-versions.py """ diff --git a/scripts/docs/sync-locale-front-matter.py b/scripts/docs/sync-locale-front-matter.py new file mode 100755 index 0000000000..4f7feacc0d --- /dev/null +++ b/scripts/docs/sync-locale-front-matter.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Restore the structural front matter of translated docs pages from English. + +Crowdin translates every front matter value in docs//, but some keys are +structure the site reads rather than prose: + + layout names a file in _layouts/ or the theme; a translated name renders + the page without the theme. + nav_order a number the theme sorts by. + +Each of these keys in docs//.md is set to what docs/en/.md +has, verbatim, including multi-line values and absence. + +parent and grand_parent are removed from every locale page, with or without an +English twin. A locale has no section pages of its own, and just-the-docs lists +every page whose parent matches a page's title in that page's table of contents, +nav_exclude or not, so a parent can only attach the locale page to an English +section. The locale_page layout links each page to its English original instead. + +Every other line is left as Crowdin wrote it. + +A locale page with no front matter while its English page has one is reported +but not rewritten: Crowdin rebuilds each translation from the English source's +structure on the next download. + +Usage: sync-locale-front-matter.py [--check] [] + +With --check nothing is written and the exit status is non-zero when any locale +page differs from English on layout or nav_order, or has a parent or grand_parent. +Under GitHub Actions each finding is also an annotation on the file. +""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +STRUCTURAL_KEYS = ("layout", "nav_order") +NAVIGATION_KEYS = ("parent", "grand_parent") +LOCALE_DIR = re.compile(r"^[a-z]{2,3}(-r[A-Za-z]+)?$") +TOP_LEVEL_KEY = re.compile(r"^([A-Za-z_][A-Za-z0-9_-]*)\s*:(\s|$)") +IN_GITHUB_ACTIONS = os.environ.get("GITHUB_ACTIONS") == "true" + +Entry = tuple[str | None, list[str]] + + +def split_lines(text: str) -> list[str]: + # str.splitlines would also split on U+2028 and friends, which are content here. + parts = text.split("\n") + lines = [part + "\n" for part in parts[:-1]] + return lines + [parts[-1]] if parts[-1] else lines + + +def content(lines: list[str]) -> list[str]: + end = len(lines) + while end > 1 and not lines[end - 1].strip(): + end -= 1 + return lines[:end] + + +class FrontMatter: + """A page split into its front matter entries, one per top-level key.""" + + def __init__(self, head: str, entries: list[Entry], tail: list[str]) -> None: + self.head = head + # Lines before the first key, if any, form an entry whose key is None. + self.entries = entries + self.tail = tail + + @classmethod + def parse(cls, text: str) -> FrontMatter | None: + lines = split_lines(text) + if not lines or lines[0].rstrip() != "---": + return None + close = next((i for i in range(1, len(lines)) if lines[i].rstrip() in ("---", "...")), None) + if close is None: + return None + entries: list[Entry] = [] + for line in lines[1:close]: + match = TOP_LEVEL_KEY.match(line) + if match: + entries.append((match.group(1), [line])) + elif entries: + entries[-1][1].append(line) + else: + entries.append((None, [line])) + return cls(lines[0], entries, lines[close:]) + + def get(self, key: str) -> list[str] | None: + return next((lines for k, lines in self.entries if k == key), None) + + def line_of(self, key: str) -> int: + line = 2 + for k, lines in self.entries: + if k == key: + return line + line += len(lines) + return 1 + + def render(self) -> str: + return self.head + "".join("".join(lines) for _, lines in self.entries) + "".join(self.tail) + + +def shown_value(lines: list[str] | None) -> str: + if lines is None: + return "(absent)" + first = lines[0].split(":", 1)[1].strip() + return first + (" ..." if len(content(lines)) > 1 else "") + + +def sync(locale: FrontMatter, english: FrontMatter) -> list[tuple[str, str, str]]: + """Rewrites locale in place and returns (key, locale value, English value) per change.""" + changes = [] + newline = "\r\n" if locale.head.endswith("\r\n") else "\n" + for key in STRUCTURAL_KEYS: + want = english.get(key) + have = locale.get(key) + if want is not None: + want = [line.rstrip("\r\n") + newline for line in content(want)] + if (content(have) if have is not None else None) == want: + continue + changes.append((key, shown_value(have), shown_value(want))) + if want is None: + locale.entries = [(k, lines) for k, lines in locale.entries if k != key] + elif have is None: + locale.entries.append((key, want)) + else: + # Keep the blank lines Crowdin left after the entry. + replacement = want + have[len(content(have)):] + locale.entries = [(k, replacement if k == key else lines) for k, lines in locale.entries] + return changes + + +def strip(locale: FrontMatter) -> list[tuple[str, str]]: + """Removes NAVIGATION_KEYS from locale in place and returns (key, value) per removed key.""" + removed = [(key, shown_value(locale.get(key))) for key in NAVIGATION_KEYS if locale.get(key) is not None] + if removed: + locale.entries = [(k, lines) for k, lines in locale.entries if k not in NAVIGATION_KEYS] + return removed + + +def annotate(level: str, path: Path, line: int, message: str) -> None: + if IN_GITHUB_ACTIONS: + print(f"::{level} file={path},line={line}::{message}") + else: + print(f"{path}:{line}: {level}: {message}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--check", action="store_true", help="report drift without writing") + parser.add_argument("docs_dir", nargs="?", type=Path, default=REPO_ROOT / "docs") + args = parser.parse_args() + + docs: Path = args.docs_dir + english_root = docs / "en" + if not english_root.is_dir(): + print(f"{english_root} does not exist", file=sys.stderr) + return 2 + + locale_roots = sorted( + p for p in docs.iterdir() if p.is_dir() and p.name != "en" and LOCALE_DIR.match(p.name) + ) + drifted = 0 + for locale_root in locale_roots: + for page in sorted(locale_root.rglob("*.md")): + source = english_root / page.relative_to(locale_root) + english = FrontMatter.parse(source.read_bytes().decode("utf-8")) if source.is_file() else None + shown = page.relative_to(REPO_ROOT) if page.is_relative_to(REPO_ROOT) else page + locale = FrontMatter.parse(page.read_bytes().decode("utf-8")) + if locale is None: + if english is None: + continue + missing = [key for key in STRUCTURAL_KEYS if english.get(key) is not None] + if args.check and missing: + drifted += 1 + annotate("error", shown, 1, f"no front matter, so no {', '.join(missing)}; docs/en has them") + else: + annotate("warning", shown, 1, "no front matter, while its docs/en page has one") + continue + lines = {key: locale.line_of(key) for key in STRUCTURAL_KEYS + NAVIGATION_KEYS} + changes = sync(locale, english) if english is not None else [] + removed = strip(locale) + if not changes and not removed: + continue + drifted += 1 + for key, have, want in changes: + if args.check: + annotate("error", shown, lines[key], f"{key} is '{have}', docs/en has '{want}'") + else: + print(f"{shown}: {key} '{have}' -> '{want}'") + for key, have in removed: + if args.check: + annotate("error", shown, lines[key], f"{key} is '{have}'; locale pages have no {key}") + else: + print(f"{shown}: {key} '{have}' removed") + if not args.check: + page.write_bytes(locale.render().encode("utf-8")) + + if args.check and drifted: + print( + f"{drifted} locale page(s) have {' or '.join(STRUCTURAL_KEYS)} unlike docs/en, " + f"or have {' or '.join(NAVIGATION_KEYS)}. " + "Fix with: python3 scripts/docs/sync-locale-front-matter.py", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/protobufs-bump-summary.py b/scripts/protobufs-bump-summary.py new file mode 100644 index 0000000000..05fbaed873 --- /dev/null +++ b/scripts/protobufs-bump-summary.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Summarise a protobufs pin bump for its pull request. + +Usage: protobufs-bump-summary.py OLD_VERSION NEW_VERSION --protobufs DIR [--before XML --after XML] + +OLD/NEW are catalog versions (2.8.0.111-g45f6b7e-SNAPSHOT or 2.8.1); DIR is a clone of meshtastic/protobufs +that has both commits. --before/--after are values/schema_strings.xml as it was and as `sync` rewrote it. +Prints Markdown: the compare link, the pull requests merged in between, what changed in the .proto files, and +which settings strings android will now show differently. +""" +import argparse +import html +import re +import subprocess +import sys + +SNAPSHOT = re.compile(r"^\d+\.\d+\.\d+\.\d+-g([0-9a-f]+)-SNAPSHOT$") +RELEASE = re.compile(r"^\d+\.\d+\.\d+$") +STRING = re.compile(r']*>(.*?)', re.S) +ADDED = { + "fields": re.compile(r"^\+\s+(?:optional\s+|repeated\s+)?[\w.]+\s+\w+\s*=\s*\d+"), + "enum values": re.compile(r"^\+\s+[A-Z][A-Z0-9_]*\s*=\s*(?:0x[0-9A-Fa-f]+|-?\d+)"), + "labels": re.compile(r"^\+.*\blabel:"), + "descriptions": re.compile(r"^\+.*\bdescription:"), + "firmware gates": re.compile(r"^\+.*\b(?:since_firmware|deprecated_since):"), +} + + +def ref(version): + m = SNAPSHOT.match(version) + if m: + return m.group(1) + if RELEASE.match(version): + return "v" + version + sys.exit(f"unrecognised protobufs version: {version}") + + +def git(repo, *args): + return subprocess.run(["git", "-C", repo, *args], check=True, capture_output=True, text=True).stdout + + +def merged_prs(repo, old, new): + out = [] + for line in git(repo, "log", "--first-parent", "--format=%s%x1f%b%x1e", f"{old}..{new}").split("\x1e"): + if not line.strip(): + continue + subject, _, body = line.strip().partition("\x1f") + m = re.match(r"Merge pull request #(\d+) from \S+", subject) + if m: + title = body.strip().splitlines()[0] if body.strip() else "" + out.append(f"- #{m.group(1)} {title}".rstrip()) + continue + m = re.search(r"\(#(\d+)\)$", subject) + out.append(f"- #{m.group(1)} {subject[: m.start()].strip()}" if m else f"- {subject}") + return out + + +def proto_changes(repo, old, new): + stat = git(repo, "diff", "--stat=100", f"{old}..{new}", "--", "meshtastic/*.proto").strip().splitlines() + diff = git(repo, "diff", "-U0", f"{old}..{new}", "--", "meshtastic/*.proto") + counts = {name: sum(1 for line in diff.splitlines() if rx.match(line)) for name, rx in ADDED.items()} + return stat, counts + + +def strings(path): + with open(path, encoding="utf-8") as f: + return {m.group(1): html.unescape(m.group(2)) for m in STRING.finditer(f.read())} + + +def schema_delta(before, after): + a, b = strings(before), strings(after) + added = sorted(k for k in b if k not in a) + removed = sorted(k for k in a if k not in b) + changed = sorted(k for k in a if k in b and a[k] != b[k]) + lines = [] + if added: + lines.append(f"**{len(added)} new strings**") + lines += [f"- `{k}`: {b[k]}" for k in added] + if changed: + lines.append(f"**{len(changed)} changed strings**") + lines += [f"- `{k}`: {a[k]} → {b[k]}" for k in changed] + if removed: + lines.append(f"**{len(removed)} removed strings** (a control still using one of these will not compile)") + lines += [f"- `{k}`" for k in removed] + return lines or ["No settings string changes."] + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("old") + p.add_argument("new") + p.add_argument("--protobufs", required=True) + p.add_argument("--before") + p.add_argument("--after") + args = p.parse_args() + old, new = ref(args.old), ref(args.new) + + out = ["", f"## protobufs `{args.old}` → `{args.new}`", ""] + out.append(f"Compare: https://github.com/meshtastic/protobufs/compare/{old}...{new}") + out.append("") + prs = merged_prs(args.protobufs, old, new) + out.append("### Merged upstream") + out += prs or ["- nothing between these commits"] + out.append("") + stat, counts = proto_changes(args.protobufs, old, new) + out.append("### Schema") + out += [f" {s}" for s in stat] or [" no .proto changes"] + added = ", ".join(f"{v} {k}" for k, v in counts.items() if v) + if added: + out.append("") + out.append(f"Added lines: {added}.") + out.append("") + if args.before and args.after: + out.append("### Settings strings (`values/schema_strings.xml`)") + out += schema_delta(args.before, args.after) + print("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/scripts/scheduled-updates/assets.sh b/scripts/scheduled-updates/assets.sh new file mode 100755 index 0000000000..7318ffd06d --- /dev/null +++ b/scripts/scheduled-updates/assets.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# The API assets the scheduled-updates PR carries, one row each in asset(). +# assets.sh fetch refreshes androidApp/src/main/assets/; writes status and detail to $GITHUB_OUTPUT +# assets.sh body writes the PR body to $GITHUB_OUTPUT from the _STATUS and _DETAIL environment +set -e + +assets_dir=androidApp/src/main/assets +keys=(firmware hardware event_firmware device_links bootloader_quirks maintenance_uf2) + +# `compare` is the jq filter both copies are diffed through and `write` the one the new copy is written through +# (empty copies it as served). Each `guards` line is a jq count that must not be 0, so a degraded response never +# replaces the bundled seed. +asset() { + guards="" guard_warning="" guard_detail="" + case "$1" in + firmware) + file=firmware_releases.json + url=https://api.meshtastic.org/github/firmware/list + fetching="firmware releases" api="firmware API" skipping="firmware update" subject="firmware list" + # The API lists every open firmware PR, which turns over several times a day; the app never reads the field + # (see NetworkFirmwareReleases). + compare='del(.pullRequests)' write='del(.pullRequests)' + ;; + hardware) + file=device_hardware.json + url=https://api.meshtastic.org/resource/deviceHardware + fetching="device hardware data" api="hardware API" skipping="hardware update" subject="hardware list" + compare=. write="" + ;; + event_firmware) + file=event_firmware.json + url=https://api.meshtastic.org/resource/eventFirmware + fetching="event firmware metadata" api="event firmware API" skipping="event firmware update" + subject="event firmware metadata" + compare=. write="" + ;; + device_links) + file=device_links.json + url=https://api.meshtastic.org/resource/deviceLinks + fetching="device links" api="device links API" skipping="device links update" subject="device links" + # The envelope carries a server-set generatedAt that changes on every response. + compare=.links write=. + guards='if (.links | type) == "array" then (.links | length) else 0 end' + guard_warning="Device links API returned no links. Skipping to protect the bundled seed." + guard_detail="empty links array from device links API" + ;; + bootloader_quirks) + file=device_bootloader_ota_quirks.json + url=https://api.meshtastic.org/resource/bootloaderOtaQuirks + fetching="bootloader OTA quirks" api="bootloader quirks API" skipping="quirks update" subject="bootloader quirks" + compare=. write=. + # softDeviceVariants gates a destructive flash, so it fails closed. + guards='if (.softDeviceVariants | type) == "array" then (.softDeviceVariants | length) else 0 end' + guard_warning="Bootloader quirks API returned no softDeviceVariants. Skipping to protect the bundled seed." + guard_detail="empty softDeviceVariants from bootloader quirks API" + ;; + maintenance_uf2) + file=maintenance_uf2.json + url=https://api.meshtastic.org/resource/maintenanceUf2 + fetching="maintenance UF2 manifest" api="maintenance UF2 API" skipping="manifest update" + subject="maintenance UF2 manifest" + compare=. write=. + # Its digest-pinned images gate destructive maintenance flashes, and without erase.nrf52Bootloader every + # offline install loses the bootloader-driven erase path. + guards='if (.otafixByBoardId | type) == "object" then (.otafixByBoardId | length) else 0 end +if (.erase | type) == "object" and (.erase.nrf52Bootloader | type) == "object" then 1 else 0 end' + guard_warning="Maintenance UF2 API response is missing the erase set (incl. erase.nrf52Bootloader) or board map. Skipping to protect the bundled seed." + guard_detail="degraded maintenance UF2 manifest (no erase set, no erase.nrf52Bootloader, or empty board map)" + ;; + *) + echo "unknown asset: $1" >&2 + exit 2 + ;; + esac +} + +output() { + echo "status=$1" >> "$GITHUB_OUTPUT" + if [ -n "$2" ]; then + echo "detail=$2" >> "$GITHUB_OUTPUT" + fi +} + +guard_tripped() { + local expr count + while IFS= read -r expr; do + [ -n "$expr" ] || continue + count=$(jq -r "$expr" "$tmp" 2>/dev/null || echo 0) + if [ "$count" -eq 0 ]; then + return 0 + fi + done <<< "$guards" + return 1 +} + +fetch() { + asset "$1" + path=$assets_dir/$file + tmp=/tmp/new_$file + + echo "Fetching latest $fetching..." + http_code=$(curl -s --max-time 90 -o "$tmp" -w '%{http_code}' "$url" || true) + http_code="${http_code:-0}" + + if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then + echo "::warning::${api^} returned HTTP $http_code. Skipping $skipping." + output error "HTTP $http_code from $api" + elif ! jq empty "$tmp" 2>/dev/null; then + echo "::warning::${api^} returned invalid JSON data. Skipping $skipping." + output error "Invalid JSON response from $api" + elif guard_tripped; then + echo "::warning::$guard_warning" + output error "$guard_detail" + elif [ ! -f "$path" ] || ! jq --sort-keys "$compare" "$tmp" | diff -q - <(jq --sort-keys "$compare" "$path"); then + echo "Changes detected in $subject or local file missing. Updating $path." + if [ -n "$write" ]; then + jq "$write" "$tmp" > "$path" + else + cp "$tmp" "$path" + fi + output updated + else + echo "No changes detected in $subject." + output unchanged + fi +} + +body() { + local key status_var detail_var status detail body + body="This PR includes automated updates from the scheduled workflow:" + body+=$'\n' + + for key in "${keys[@]}"; do + asset "$key" + status_var=${key^^}_STATUS detail_var=${key^^}_DETAIL + status=${!status_var} detail=${!detail_var} + case "$status" in + updated) body+=$'\n'"- ✅ \`$file\` updated from the Meshtastic API." ;; + unchanged) body+=$'\n'"- ✔️ \`$file\` checked and unchanged." ;; + error) body+=$'\n'"- ⚠️ \`$file\` skipped ($detail)." ;; + *) body+=$'\n'"- ❓ \`$file\` status unknown." ;; + esac + done + + case "$OBTAINIUM_STATUS" in + updated) body+=$'\n'"- ✅ Obtainium deep links and import files regenerated (channel status changed)." ;; + unchanged) body+=$'\n'"- ✔️ Obtainium configs checked against the live releases and unchanged." ;; + error) body+=$'\n'"- ⚠️ Obtainium configs skipped ($OBTAINIUM_DETAIL)." ;; + *) body+=$'\n'"- ❓ Obtainium configs status unknown." ;; + esac + + if [[ "$SCHEMA_PIN_CHANGED" == "true" ]]; then + case "$SCHEMA_STRINGS_STATUS" in + updated) body+=$'\n'"- ✅ \`schema_strings.xml\` regenerated for protobufs \`$SCHEMA_PIN_CATALOG\` ($SCHEMA_STRINGS_DETAIL)." ;; + error) body+=$'\n'"- ⚠️ \`schema_strings.xml\` skipped ($SCHEMA_STRINGS_DETAIL)." ;; + *) body+=$'\n'"- ❓ \`schema_strings.xml\` status unknown." ;; + esac + else + body+=$'\n'"- ✔️ \`schema_strings.xml\` already reflects protobufs \`$SCHEMA_PIN_CATALOG\`." + fi + + body+=$'\n'"- Source strings were uploaded to Crowdin." + body+=$'\n'"- Latest translations were downloaded from Crowdin (if available)." + body+=$'\n' + body+=$'\n'"Please review the changes." + + { + echo "content<> "$GITHUB_OUTPUT" +} + +case "$1" in + fetch) fetch "$2" ;; + body) body ;; + *) + echo "usage: $0 fetch <${keys[*]}> | body" >&2 + exit 2 + ;; +esac diff --git a/scripts/sort-strings.py b/scripts/sort-strings.py index 6c20ddd625..bed4288a16 100644 --- a/scripts/sort-strings.py +++ b/scripts/sort-strings.py @@ -6,7 +6,7 @@ import re # Usage: python3 scripts/sort-strings.py # This script alphabetizes strings.xml, adds prefix markers, and regenerates strings-index.txt. -def sort_strings(xml_path, index_path): +def sort_strings(xml_path, index_path, schema_xml_path=None): print(f"Reading {xml_path}...") with open(xml_path, 'r', encoding='utf-8', newline='\n') as f: content = f.read() @@ -83,6 +83,13 @@ def sort_strings(xml_path, index_path): f.write(final_content) print(f"Successfully sorted {xml_path}") + # Index the generated schema strings too, so a lookup finds every resource the app has. That file is written + # by ./gradlew :schema-strings:sync and is already sorted, so it is only read here. + if schema_xml_path and os.path.exists(schema_xml_path): + schema_names = [n for n in (child.get('name') for child in ET.parse(schema_xml_path).getroot()) if n] + index_lines.append("### SCHEMA (generated, values/schema_strings.xml) ###") + index_lines.extend(sorted(schema_names)) + # Write Index with open(index_path, 'w', encoding='utf-8', newline='\n') as f: f.write('\n'.join(index_lines) + '\n') @@ -90,9 +97,10 @@ def sort_strings(xml_path, index_path): if __name__ == "__main__": xml_file = 'core/resources/src/commonMain/composeResources/values/strings.xml' + schema_xml_file = 'core/resources/src/commonMain/composeResources/values/schema_strings.xml' index_file = '.skills/compose-ui/strings-index.txt' if os.path.exists(xml_file): - sort_strings(xml_file, index_file) + sort_strings(xml_file, index_file, schema_xml_file) else: print(f"Error: {xml_file} not found.") diff --git a/scripts/sync-play-changelog.py b/scripts/sync-play-changelog.py index 650a7b3076..ed46b1dfcb 100755 --- a/scripts/sync-play-changelog.py +++ b/scripts/sync-play-changelog.py @@ -12,7 +12,8 @@ source file every internal build. Play falls back to it for any build without a version-specific file, which is all of them here. Run it in the PR that bumps VERSION_NAME_BASE, so Crowdin has the whole internal -cycle to translate before a production promotion uploads anything. +cycle to translate. Every promotion (closed, open, production) uploads the file for +each locale onto the promoted release. python3 scripts/sync-play-changelog.py [--check] """ diff --git a/scripts/verify-flatpak/README.md b/scripts/verify-flatpak/README.md index 7a8120253e..0253f42808 100644 --- a/scripts/verify-flatpak/README.md +++ b/scripts/verify-flatpak/README.md @@ -79,3 +79,6 @@ executing the Gradle build, or run the full script on a Linux host. - `desktop-offline.yaml` — patched manifest. Kept in sync manually with the upstream packaging; diff against `https://raw.githubusercontent.com/flathub/org.meshtastic.MeshtasticDesktop/master/org.meshtastic.MeshtasticDesktop.yaml` if upstream changes something material. +- `bump-flathub-manifest.py` - points the upstream manifest at a release: the source tag and + commit, and the Gradle zip the tag's wrapper pins. `promote.yml`'s `update-flathub` job runs it + on every production promotion, then commits the release's `flatpak-sources.json` beside it. diff --git a/scripts/verify-flatpak/bump-flathub-manifest.py b/scripts/verify-flatpak/bump-flathub-manifest.py new file mode 100755 index 0000000000..e346d51ad8 --- /dev/null +++ b/scripts/verify-flatpak/bump-flathub-manifest.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Point the Flathub manifest at a release: the source tag and commit, and the Gradle zip. + +Usage: bump-flathub-manifest.py + +The Gradle distribution URL and sha256 come from the tag's own wrapper properties, since the +offline build can only run the Gradle version that tag pins. Each field is rewritten in +place, so comments and layout survive. Any field that does not match exactly once fails the +run instead of guessing, and the manifest is then bumped by hand. +""" + +import re +import sys + +GIT_SOURCE = re.compile( + r"^(?P\s+url: https://github\.com/meshtastic/Meshtastic-Android\.git\n" + r"\s+tag: )\S+(?P\n\s+commit: )[0-9a-f]{40}$", + re.MULTILINE, +) +GRADLE_ZIP = re.compile( + r"^(?P\s+url: )https://services\.gradle\.org/distributions/gradle-[^\s/]+\.zip" + r"(?P\n\s+sha256: )[0-9a-f]{64}$", + re.MULTILINE, +) + + +def wrapper_distribution(path: str) -> tuple[str, str]: + props = {} + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + props[key.strip()] = value.strip().replace("\\:", ":") + url = props.get("distributionUrl", "") + sha = props.get("distributionSha256Sum", "") + if not re.fullmatch(r"https://services\.gradle\.org/distributions/gradle-[^\s/]+\.zip", url): + sys.exit(f"{path}: distributionUrl '{url}' is not a services.gradle.org distribution") + if not re.fullmatch(r"[0-9a-f]{64}", sha): + sys.exit(f"{path}: distributionSha256Sum '{sha}' is not a sha256") + return url, sha + + +def replace_once(pattern: re.Pattern, value_a: str, value_b: str, text: str, what: str) -> str: + new, count = pattern.subn(lambda m: m["lead"] + value_a + m["mid"] + value_b, text) + if count != 1: + sys.exit(f"{what}: expected exactly one match in the manifest, found {count}") + return new + + +def main() -> None: + if len(sys.argv) != 5: + sys.exit(__doc__) + manifest, tag, commit, wrapper = sys.argv[1:] + if not re.fullmatch(r"v\d+\.\d+\.\d+", tag): + sys.exit(f"'{tag}' is not a production tag (vX.Y.Z)") + if not re.fullmatch(r"[0-9a-f]{40}", commit): + sys.exit(f"'{commit}' is not a full commit sha") + url, sha = wrapper_distribution(wrapper) + + with open(manifest, encoding="utf-8") as f: + text = f.read() + text = replace_once(GIT_SOURCE, tag, commit, text, "Meshtastic-Android git source (url, tag, commit)") + text = replace_once(GRADLE_ZIP, url, sha, text, "Gradle distribution (url, sha256)") + with open(manifest, "w", encoding="utf-8") as f: + f.write(text) + print(f"{manifest}: {tag} at {commit}, {url.rsplit('/', 1)[-1]}") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify-flatpak/desktop-offline.yaml b/scripts/verify-flatpak/desktop-offline.yaml index 779b8056cc..6d8687d5aa 100644 --- a/scripts/verify-flatpak/desktop-offline.yaml +++ b/scripts/verify-flatpak/desktop-offline.yaml @@ -101,8 +101,8 @@ modules: # flavor); distributionSha256Sum verifies this file. Renovate bumps the version here # alongside wrapper bumps (custom manager in renovate.json) but the sha256 must be # copied from distributionSha256Sum by hand; CI fails fast on any mismatch. - url: https://services.gradle.org/distributions/gradle-9.7.1-bin.zip - sha256: acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a + url: https://services.gradle.org/distributions/gradle-9.8.0-bin.zip + sha256: bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c dest: "gradle/wrapper" dest-filename: "gradle-bin.zip" - flatpak-sources.json diff --git a/scripts/verify-rb-selftest.sh b/scripts/verify-rb-selftest.sh index 4830e13f57..684ac13175 100755 --- a/scripts/verify-rb-selftest.sh +++ b/scripts/verify-rb-selftest.sh @@ -4,7 +4,7 @@ # Step 6 itself only runs in the merge queue and needs two full release builds, so the # classification it depends on would otherwise ship unexercised: a typo in the allowlist # silently brings the noise back, and a broken dedup silently restores per-ABI repeats. -# This runs in lint-check on every PR instead. +# This runs in pull-request.yml's check-metadata job on every PR instead. # # readelf is stubbed: the fixture writes the literal STRIPPED into a lib to mean "no # .symtab", anything else means the symbol table survived. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1f5c995bb8..8a8976a299 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -125,6 +125,7 @@ include( ":feature:widget", ":screenshot-tests", ":docs-screenshots", - ":marketing-screenshots", + ":schema-strings", ":baselineprofile", + ":store-screenshots", ) diff --git a/specs/005-tak-v2-protocol/plan.md b/specs/005-tak-v2-protocol/plan.md index 9f1c890d7a..dd54ff65b2 100644 --- a/specs/005-tak-v2-protocol/plan.md +++ b/specs/005-tak-v2-protocol/plan.md @@ -12,7 +12,7 @@ Upgrades Meshtastic Android's TAK integration from legacy v1 (port 72, PLI + Geo **Language/Version**: Kotlin 2.3+ targeting JDK 21 (KMP multi-target) **Primary Dependencies**: TAKPacket-SDK v0.1.3 (zstd compression), xmlutil (CoT XML parsing), Ktor Network (TCP), zstd-jni 1.5.7-7, Okio (I/O), Koin 4.2+ (DI), Kermit (logging) -**Storage**: App-private filesystem for route KML data packages; bundled .p12/.pem certificates for TLS +**Storage**: Downloads (MediaStore) for route KML data packages; bundled .p12/.pem certificates for TLS **Testing**: `commonTest` (9 test classes, 65+ test methods), 40 XML fixture files in `jvmAndroidMain/resources/tak_test_fixtures/` **Target Platform**: Android (primary), JVM Desktop (secondary), iOS (stubs only) **Project Type**: Mobile app — KMP module (`core:takserver`) + UI integration (`feature:settings`) @@ -26,7 +26,7 @@ Upgrades Meshtastic Android's TAK integration from legacy v1 (port 72, PLI + Geo - **I. Kotlin Multiplatform Core**: ✅ All business logic (TAKMeshIntegration, conversions, type mapper, CoT parser, detail stripper, server manager, models, DI module) resides in `commonMain`. Platform-specific code isolated to: - `jvmAndroidMain`: TAKServerJvm (JSSE TLS), TakV2Compressor (zstd-jni via SDK), TakCertLoader, TAKClientConnection - - `androidMain`: AtakFileWriter (SAF/private dirs), TakPermissionUtil (runtime permissions) + - `androidMain`: AtakFileWriter (MediaStore Downloads), TakPermissionUtil (runtime permissions) - `jvmMain`: AtakFileWriter (desktop filesystem), TakPermissionUtil (no-op) - `iosMain`: TAKServerIos (no-op), TakV2Compressor (uncompressed stub), AtakFileWriter (stub) @@ -115,7 +115,7 @@ core/takserver/ │ ├── tak_certs/ # Bundled mTLS certificates │ └── tak_test_fixtures/ # 40 CoT XML fixtures ├── androidMain/kotlin/.../ - │ └── AtakFileWriter.kt # SAF/private directory writer + │ └── AtakFileWriter.kt # MediaStore Downloads writer ├── jvmMain/kotlin/.../ │ └── AtakFileWriter.kt # Desktop filesystem writer └── iosMain/kotlin/.../ diff --git a/specs/005-tak-v2-protocol/spec.md b/specs/005-tak-v2-protocol/spec.md index 4946edcc26..868bb05f94 100644 --- a/specs/005-tak-v2-protocol/spec.md +++ b/specs/005-tak-v2-protocol/spec.md @@ -16,7 +16,7 @@ This feature upgrades the Meshtastic Android app's TAK (Team Awareness Kit) inte 2. **Efficient wire encoding**: Use zstd dictionary compression and CoT detail stripping to fit rich CoT payloads within the LoRa MTU constraint (237 bytes raw, ~225 bytes usable after protobuf framing overhead) 3. **Backward compatibility**: Auto-detect firmware version and gracefully fall back to legacy TAKPacket (v1) for radios running firmware < 2.8.0 4. **Reliable TAK server operation**: Maintain a local TLS/mTLS TAK server that ATAK and iTAK clients can connect to, with wake lock protection against Android battery optimization -5. **Route interoperability**: Bridge ATAK's route CoT limitation by generating KML data packages for auto-import into ATAK's monitored directory +5. **Route interoperability**: Bridge ATAK's route CoT limitation by generating KML data packages saved to Downloads for the user to import into ATAK ## Non-Goals @@ -136,7 +136,7 @@ A v2-capable node receives packets from both v1 (port 72) and v2 (port 78) mesh | RouteDataPackageGenerator | `core/takserver/…/RouteDataPackageGenerator.kt` (commonMain) | Converts route CoT to ATAK-importable KML data packages | | CoTXmlParser | `core/takserver/…/CoTXmlParser.kt` (commonMain) | Streaming XML parser for inbound CoT from ATAK clients | | XmlUtils | `core/takserver/…/XmlUtils.kt` (commonMain) | XML escaping/sanitization utilities (5 special characters) | -| AtakFileWriter | `core/takserver/…/AtakFileWriter.kt` (expect/actual) | Platform filesystem access: androidMain (SAF/private dirs), jvmMain (desktop filesystem), iosMain (stub) | +| AtakFileWriter | `core/takserver/…/AtakFileWriter.kt` (expect/actual) | Saves route data packages: androidMain (Downloads via MediaStore from API 29, the app's external Downloads folder below it), jvmMain and iosMain (no-op) | | TAKConfigItemList | `feature/settings/…/TAKConfigItemList.kt` (commonMain) | Compose UI for TAK module configuration | | TakPermissionUtil | `feature/settings/…/TakPermissionUtil.kt` (expect/actual) | Platform-specific permission handling (Android, iOS, JVM) | | MeshService (wake lock) | `core/service/MeshService.kt` (androidMain) | Partial wake lock for reliable TAK server operation | @@ -168,14 +168,14 @@ A v2-capable node receives packets from both v1 (port 72) and v2 (port 78) mesh - **NFR-001**: Compressed TAKPacketV2 payloads MUST fit within the usable mesh payload (~225 bytes after protobuf framing within the 237-byte raw LoRa MTU) for single-packet transmission - **NFR-002**: TAK server connection MUST survive screen-off and Doze mode for at least 30 minutes without disconnection - **NFR-003**: CoT message round-trip (ATAK → mesh → remote ATAK) MUST complete within the mesh network's standard transmission latency (no added processing delay > 100ms) -- **NFR-004**: Route data packages MUST be written to app-private or cache directories (no MANAGE_EXTERNAL_STORAGE required); ATAK integration relies on content sharing or documented import paths +- **NFR-004**: Route data packages MUST be saved without any storage permission: to Downloads through MediaStore from API 29, and to the app's own external Downloads folder below it. The user imports them into ATAK by hand; the app writes nothing into ATAK's own directories ## Source-Set Impact | Source Set | Impact | Justification | |-----------|--------|---------------| | `commonMain` | All business logic: TAKMeshIntegration, conversions, models, parser, server manager, detail stripper, XML utils, config UI | All business logic and UI per Constitution §I, §III | -| `androidMain` | MeshService wake lock, AtakFileWriter (Android filesystem/SAF), TakPermissionUtil (runtime permissions) | Platform-specific Android APIs | +| `androidMain` | MeshService wake lock, AtakFileWriter (MediaStore Downloads), TakPermissionUtil (runtime permissions) | Platform-specific Android APIs | | `jvmAndroidMain` | TAKServerJvm TLS implementation, TakV2Compressor (zstd via TAKPacket-SDK), TakCertLoader, TakFixtureLoader | Shared JVM/Android TLS, compression, and I/O | | `jvmMain` | AtakFileWriter (desktop filesystem), TakPermissionUtil (no-op) | Desktop platform support for file operations | | `iosMain` | TAKServerIos, TakV2Compressor (stub — uncompressed TAK_TRACKER mode only), AtakFileWriter (stub), TakFixtureLoader | Platform stubs pending Swift SDK integration | @@ -214,7 +214,7 @@ A v2-capable node receives packets from both v1 (port 72) and v2 (port 78) mesh - ATAK clients support standard TAK Server protocol (TLS on port 8089, data package import) - Zstd dictionaries are pre-trained and bundled as binary resources (not trained at runtime) - The 237-byte raw LoRa MTU is a hard limit imposed by the radio hardware; usable payload is ~225 bytes after protobuf framing -- Route data packages are written to app-private/cache directories (no broad filesystem permissions required) +- Route data packages are saved to Downloads for manual import into ATAK (no storage permission required) - iOS implementation uses uncompressed TAK_TRACKER mode (flags=0xFF) pending platform-specific zstd library integration via Swift SDK interop - Desktop (JVM) has partial TAK support: filesystem operations via `jvmMain` AtakFileWriter, TLS server via `jvmAndroidMain` - Android 17+ (API 37) requires ACCESS_LOCAL_NETWORK permission for TAK server localhost binding diff --git a/specs/20260711-153545-message-markdown-styling/quickstart.md b/specs/20260711-153545-message-markdown-styling/quickstart.md index 9043f26685..5c16b6663b 100644 --- a/specs/20260711-153545-message-markdown-styling/quickstart.md +++ b/specs/20260711-153545-message-markdown-styling/quickstart.md @@ -39,8 +39,8 @@ Delegate heavy Gradle to the `gradle-runner` subagent; **git-diff-verify after** Single-module fast loops: ```bash -./gradlew :core:ui:allTests --tests "*InlineMarkdown*" -./gradlew :feature:messaging:allTests --tests "*MessageFormatting*" +./gradlew :core:ui:jvmTest --tests "*InlineMarkdown*" +./gradlew :feature:messaging:jvmTest --tests "*MessageFormatting*" ``` ### Live verification (mandatory — /verify skill) diff --git a/store-screenshots/build.gradle.kts b/store-screenshots/build.gradle.kts new file mode 100644 index 0000000000..d4d126c720 --- /dev/null +++ b/store-screenshots/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Captures the store-listing screenshots from the real debug app on a device or emulator: +// ./gradlew :store-screenshots:connectedGoogleDebugAndroidTest (Play) +// ./gradlew :store-screenshots:connectedFdroidDebugAndroidTest (F-Droid, IzzyOnDroid) +// PNGs are left on the device in /data/local/tmp/store-screenshots/, laid out like fastlane's images/; pull +// them with +// adb pull /data/local/tmp/store-screenshots//. +// .github/workflows/store-screenshots.yml runs each flavor on its own emulator through capture-android.sh. +plugins { + alias(libs.plugins.meshtastic.android.test) + alias(libs.plugins.meshtastic.detekt) + alias(libs.plugins.meshtastic.spotless) +} + +android { + namespace = "org.meshtastic.storescreenshots" + + defaultConfig { minSdk = 28 } + + targetProjectPath = ":androidApp" + // Its own process, so relaunching the app between form factors does not end the run. + experimentalProperties["android.experimental.self-instrumenting"] = true + + flavorDimensions += "marketplace" + productFlavors { + create("google") { + dimension = "marketplace" + testInstrumentationRunnerArguments["targetAppId"] = "com.geeksville.mesh.google.debug" + testInstrumentationRunnerArguments["flavor"] = "google" + } + create("fdroid") { + dimension = "marketplace" + testInstrumentationRunnerArguments["targetAppId"] = "com.geeksville.mesh.fdroid.debug" + testInstrumentationRunnerArguments["flavor"] = "fdroid" + } + } +} + +// Both flavors can capture on one device in one invocation; each run resizes that device's display. +tasks + .named { it == "connectedFdroidDebugAndroidTest" } + .configureEach { mustRunAfter("connectedGoogleDebugAndroidTest") } + +dependencies { + implementation(libs.androidx.test.ext.junit) + implementation(libs.androidx.test.runner) + implementation(libs.androidx.uiautomator) +} diff --git a/store-screenshots/capture-android.sh b/store-screenshots/capture-android.sh new file mode 100755 index 0000000000..d28c21a82f --- /dev/null +++ b/store-screenshots/capture-android.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 Meshtastic LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Captures one flavor's store screenshots on the connected device from APKs already built +# by `:androidApp:assembleDebug :store-screenshots:assembleDebug`. +# +# capture-android.sh +# +# The PNGs land in /. The instrumentation output and the device logcat +# land in /test-results/, since no Gradle connected task writes a report. +set -euo pipefail + +FLAVOR=${1:?usage: capture-android.sh } +OUT=${2:?usage: capture-android.sh } +SHOTS="$OUT/$FLAVOR" +RESULTS="$OUT/test-results/$FLAVOR" +DEVICE_OUTPUT=/data/local/tmp/store-screenshots/$FLAVOR +mkdir -p "$SHOTS" "$RESULTS" + +app_apk=$(find "androidApp/build/outputs/apk/$FLAVOR/debug" -name '*-universal-debug.apk' | head -1) +test_apk=$(find "store-screenshots/build/outputs/apk/$FLAVOR/debug" -name '*.apk' | head -1) +[ -n "$app_apk" ] && [ -n "$test_apk" ] || { echo "::error::$FLAVOR APKs not built"; exit 1; } + +adb install -r -t "$app_apk" +adb install -r -t "$test_apk" +runner=$(adb shell pm list instrumentation | tr -d '\r' | sed -n 's/^instrumentation:\(org\.meshtastic\.storescreenshots\/[^ ]*\).*/\1/p' | head -1) +[ -n "$runner" ] || { echo "::error::no store-screenshots instrumentation on the device"; exit 1; } + +adb logcat -c || true +# am instrument exits 0 whatever the test does; the verdict is its "OK (1 test)" line. +adb shell am instrument -w -r \ + -e targetAppId "com.geeksville.mesh.$FLAVOR.debug" \ + -e flavor "$FLAVOR" \ + "$runner" | tr -d '\r' | tee "$RESULTS/instrument.txt" +adb logcat -d > "$RESULTS/logcat.txt" + +adb pull "$DEVICE_OUTPUT/." "$SHOTS/" || true +grep -q '^OK (1 test)' "$RESULTS/instrument.txt" diff --git a/store-screenshots/capture-desktop.sh b/store-screenshots/capture-desktop.sh new file mode 100755 index 0000000000..8364c7b447 --- /dev/null +++ b/store-screenshots/capture-desktop.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 Meshtastic LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Captures the five Flathub screenshots from the real desktop app (a debug build) on a +# virtual display, connected to Demo Mode's hidden showcase mesh. +# +# capture-desktop.sh +# +# Needs Xvfb, openbox, xdotool, ImageMagick and Mesa (GLX, zink, lavapipe). Each shot is +# its own launch with that screen's deep link, so nothing is clicked. A frame is kept once +# the window has not changed for a while; the map gets longer for its tiles. +set -euo pipefail + +APP=${1:?usage: capture-desktop.sh } +OUT=${2:?usage: capture-desktop.sh } +mkdir -p "$OUT" +LOG="$OUT/app.log" + +export DISPLAY=:99 +Xvfb :99 -screen 0 1600x1000x24 +extension GLX +render -noreset & +sleep 2 +# Without a window manager the window can't be placed or sized. +openbox & +sleep 2 + +# The map needs Skiko's OpenGL renderer, and Skiko refuses any GL adapter named llvmpipe +# or virgl. Zink over lavapipe is software GL under another name. +export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=zink MESA_LOADER_DRIVER_OVERRIDE=zink +export JAVA_TOOL_OPTIONS="-Dskiko.renderApi=OPENGL" + +PID= +WID= + +launch() { + "$APP" "$@" >>"$LOG" 2>&1 & + PID=$! + WID= + for _ in $(seq 1 90); do + # --onlyvisible: a hidden window can carry the same title and would swallow every command. + WID=$(xdotool search --onlyvisible --name '^Meshtastic Desktop$' 2>/dev/null | head -1) || true + [ -n "$WID" ] && break + sleep 1 + done + if [ -z "$WID" ]; then + echo "::error::the desktop app never opened a window" >&2 + exit 1 + fi + xdotool windowmove "$WID" 0 0 + xdotool windowsize "$WID" 1280 800 +} + +# settle : waits until the window has not changed for s. +settle() { + local minimum=$1 maximum=$2 stable=$3 previous='' same=0 elapsed + sleep "$minimum" + elapsed=$minimum + while [ "$elapsed" -lt "$maximum" ]; do + import -window "$WID" "$OUT/.frame.png" + local current + current=$(md5sum <"$OUT/.frame.png") + if [ "$current" = "$previous" ]; then + same=$((same + 2)) + [ "$same" -ge "$stable" ] && return 0 + else + same=0 + previous=$current + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + echo "::warning::the window never settled within ${maximum}s; capturing as is" +} + +shoot() { + import -window "$WID" "$OUT/meshtastic-desktop-$1.png" + echo "captured meshtastic-desktop-$1.png" +} + +quit() { + kill "$PID" 2>/dev/null || true + wait "$PID" 2>/dev/null || true + sleep 3 +} + +# Connect once. The address is saved, so every later launch reconnects by itself. +launch "https://meshtastic.org/connections?address=mshowcase" --skip-connect-confirm +settle 20 90 6 +shoot 04-connections +quit + +for shot in "01-nodes nodes" "02-messages messages/0^all" "05-settings settings"; do + read -r name path <<<"$shot" + launch "https://meshtastic.org/$path" + settle 15 60 6 + shoot "$name" + quit +done + +launch "https://meshtastic.org/map" +settle 45 180 10 +shoot 03-map +quit + +rm -f "$OUT/.frame.png" diff --git a/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt b/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt new file mode 100644 index 0000000000..94ddcd6197 --- /dev/null +++ b/store-screenshots/src/main/kotlin/org/meshtastic/storescreenshots/StoreScreenshots.kt @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.storescreenshots + +import android.graphics.Bitmap +import android.os.ParcelFileDescriptor +import android.os.SystemClock +import android.util.Log +import android.view.accessibility.AccessibilityNodeInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.Configurator +import androidx.test.uiautomator.UiAutomatorTestScope +import androidx.test.uiautomator.uiAutomator +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File + +/** + * The store-listing screenshots, taken from the real debug app connected to Demo Mode's hidden showcase mesh. + * + * Every screen is reached by a deep link rather than by tapping labelled tabs, so the flow does not depend on the + * display language. Each capture is the full screen, status bar included, taken once the app's window has stopped + * changing. + */ +@RunWith(AndroidJUnit4::class) +class StoreScreenshots { + + private val arguments = InstrumentationRegistry.getArguments() + private val appId = requireNotNull(arguments.getString("targetAppId")) { "targetAppId argument missing" } + private val flavor = requireNotNull(arguments.getString("flavor")) { "flavor argument missing" } + + // Shared media storage: the one app directory the shell user can read, so [save] can copy out of it. + @Suppress("DEPRECATION") + private val mediaDir = InstrumentationRegistry.getInstrumentation().context.externalMediaDirs.first() + + @Test + fun capturesEveryStoreScreen() = uiAutomator { + // Compose screens with an animation never go idle; waiting for idle would stall every step. + Configurator.getInstance().setWaitForIdleTimeout(0) + prepareDevice() + try { + FormFactor.entries.forEach { formFactor -> + shell("wm size ${formFactor.widthPx}x${formFactor.heightPx}") + shell("wm density ${formFactor.densityDpi}") + cleanStatusBar() + connectToShowcase() + Shot.entries.forEach { shot -> capture(formFactor, shot) } + } + } finally { + shell("wm size reset") + shell("wm density reset") + demo("exit") + shell("pm enable $LAUNCHER") + } + } + + /** + * No system dialog over a stalled launcher on a cold, software-rendered emulator, and no launcher at all: on a + * large screen its taskbar would sit along the bottom of every shot. + */ + private fun UiAutomatorTestScope.prepareDevice() { + shell("settings put global hide_error_dialogs 1") + shell("pm disable-user --user 0 $LAUNCHER") + shell("settings put global sysui_demo_allowed 1") + // SystemUI reads the setting asynchronously; a demo command sent before it has is dropped. + SystemClock.sleep(SYSTEM_UI_SETTLE_MS) + } + + /** SystemUI demo mode, sent again after every display change, which can rebuild the status bar. */ + private fun UiAutomatorTestScope.cleanStatusBar() { + // The first burst after demo mode is allowed is dropped on a cold emulator (measured: the first form factor + // kept the real clock, the later ones took the demo); a second burst after a settle lands. + repeat(DEMO_BURSTS) { + SystemClock.sleep(SYSTEM_UI_SETTLE_MS) + demo("enter") + demo("clock -e hhmm 0941") + demo("battery -e level 100 -e plugged false") + demo("network -e wifi show -e level 4 -e fully true -e mobile show -e datatype none -e level 4") + demo("notifications -e visible false") + } + SystemClock.sleep(SYSTEM_UI_SETTLE_MS) + } + + private fun UiAutomatorTestScope.demo(command: String) = + shell("am broadcast -a com.android.systemui.demo -e command $command") + + /** + * Relaunches the app into the showcase mesh and waits for our node's name. The launch switches skip onboarding and + * the trust dialog; if either still appears it is logged and handled rather than failing the run. + */ + private fun UiAutomatorTestScope.connectToShowcase() { + repeat(CONNECT_ATTEMPTS) { attempt -> + shell("am force-stop $appId") + open("connections?address=$SHOWCASE_ADDRESS", clearTask = true) + val deadline = SystemClock.uptimeMillis() + CONNECT_TIMEOUT_MS + while (SystemClock.uptimeMillis() < deadline) { + if (onElementOrNull(POLL_MS) { hasText(SHOWCASE_NODE_NAME) } != null) return + if (onElementOrNull(0) { hasText(TRUST_DIALOG_TITLE) } != null) { + Log.w(TAG, "the trust dialog survived skip_connect_confirm on attempt ${attempt + 1}; confirming") + onElementOrNull(0) { hasText(TRUST_DIALOG_CONFIRM) }?.click() + } + if (onElementOrNull(0) { hasText(ONBOARDING_START) } != null) { + Log.w(TAG, "onboarding survived skip_onboarding on attempt ${attempt + 1}; relaunching") + break + } + } + } + error("the app never showed $SHOWCASE_NODE_NAME after $CONNECT_ATTEMPTS launches") + } + + private fun UiAutomatorTestScope.capture(formFactor: FormFactor, shot: Shot) { + if (shot.readFirst) { + // A thread with unread messages opens at the first of them, behind a "new messages below" divider and a + // jump-to-latest pill. Opening it once marks it read; the capture then opens it at the latest message. + open(shot.path) + SystemClock.sleep(READ_WARM_UP_MS) + open(Shot.Nodes.path) + SystemClock.sleep(READ_WARM_UP_MS) + } + if (shot.waitsForMapDrawn) openAndAwaitMapDrawn(shot) else open(shot.path) + SystemClock.sleep(shot.minimumWaitMs) + val stable = + waitForStableInActiveWindow( + stableTimeoutMs = shot.stableTimeoutMs, + stableIntervalMs = shot.stableIntervalMs, + stablePollIntervalMs = POLL_MS, + requireStableScreenshot = true, + ) + check(device.currentPackageName == appId) { + "${device.currentPackageName} is in front instead of $appId while capturing ${shot.fileName}" + } + val name = "${formFactor.folder}/${shot.fileName}.png" + if (stable.isTimeout) Log.w(TAG, "$name never settled; capturing as is") + val bitmap = requireNotNull(device.takeScreenshot()) { "no screenshot for $name" } + save(bitmap, name) + } + + /** + * Waits for the map to log that its tiles are drawn: Google Maps' onMapLoaded, MapLibre's first idle. The stability + * check after it still covers the camera settling on the mesh. + */ + private fun UiAutomatorTestScope.openAndAwaitMapDrawn(shot: Shot) { + // A time boundary, not a line count: logcat is a ring buffer and older matches rotate out. + val since = System.currentTimeMillis().let { "%d.%03d".format(it / MILLIS_PER_SECOND, it % MILLIS_PER_SECOND) } + open(shot.path) + val deadline = SystemClock.uptimeMillis() + MAP_DRAWN_TIMEOUT_MS + while (SystemClock.uptimeMillis() < deadline) { + if (MAP_DRAWN_MESSAGE in shell("logcat -d -T $since -s $MAP_DRAWN_TAG")) return + SystemClock.sleep(POLL_MS) + } + Log.w(TAG, "${shot.fileName} never logged $MAP_DRAWN_TAG; capturing after the timeout") + } + + /** Launches through the debug build's shell-only alias, the one launch the app honours the switches on. */ + private fun UiAutomatorTestScope.open(path: String, clearTask: Boolean = false) { + val flags = if (clearTask) "--activity-clear-task " else "" + shell( + "am start -W $flags-a android.intent.action.VIEW -d https://meshtastic.org/$path " + + "-n $appId/org.meshtastic.app.AutomationLauncher " + + "--ez skip_onboarding true --ez skip_connect_confirm true", + ) + } + + private fun UiAutomatorTestScope.shell(command: String): String = + ParcelFileDescriptor.AutoCloseInputStream(uiAutomation.executeShellCommand(command)).use { + it.bufferedReader().readText() + } + + /** + * Writes into this package's media directory, then copies to [DEVICE_OUTPUT], which the shell owns: the media + * directory goes with the uninstall at the end of a connected run, and the workflow pulls from the copy. + */ + private fun UiAutomatorTestScope.save(bitmap: Bitmap, name: String) { + val file = File(mediaDir, name) + file.parentFile?.mkdirs() + file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, PNG_QUALITY, it) } + val target = "$DEVICE_OUTPUT/$flavor/$name" + shell("mkdir -p ${target.substringBeforeLast('/')}") + shell("cp ${file.path} $target") + Log.i(TAG, "saved $target") + } + + private fun AccessibilityNodeInfo.hasText(value: String) = text?.toString() == value + + /** The three surfaces `fastlane supply` uploads, at the sizes the store asks for. */ + @Suppress("detekt:MagicNumber") + private enum class FormFactor(val folder: String, val widthPx: Int, val heightPx: Int, val densityDpi: Int) { + Phone("phoneScreenshots", 1080, 1920, 400), + SevenInch("sevenInchScreenshots", 1080, 1920, 288), + TenInch("tenInchScreenshots", 2560, 1440, 320), + } + + /** The five listing shots, named as fastlane lays them out. The map waits for its tiles before it settles. */ + private enum class Shot( + val fileName: String, + val path: String, + val minimumWaitMs: Long = 2_000, + val stableTimeoutMs: Long = 30_000, + val stableIntervalMs: Long = 2_000, + val readFirst: Boolean = false, + val waitsForMapDrawn: Boolean = false, + ) { + // The primary channel's contact key, raw: `am start` takes it literally and Uri.parse accepts the caret. + Messages("1_messages", "messages/0^all", readFirst = true), + Nodes("2_nodes", "nodes"), + Map("3_map", "map", stableTimeoutMs = 120_000, stableIntervalMs = 8_000, waitsForMapDrawn = true), + NodeDetail("4_node_detail", "nodes/$RIDGE_TOP_NUM"), + Channels("5_channels", "channels"), + } + + private companion object { + const val TAG = "StoreScreenshots" + + /** Where the captures are left for `adb pull`: a folder per flavor, laid out as fastlane's `images/`. */ + const val DEVICE_OUTPUT = "/data/local/tmp/store-screenshots" + + /** Demo Mode's hidden showcase mesh; `MockScenario.SHOWCASE` in `:core:network`. */ + const val SHOWCASE_ADDRESS = "mshowcase" + const val SHOWCASE_NODE_NAME = "Base Camp" + + /** Ridge Top, the showcase's router with a full detail page. */ + const val RIDGE_TOP_NUM = 0xe1e22a35.toInt() + + const val TRUST_DIALOG_TITLE = "Connect to this device?" + const val TRUST_DIALOG_CONFIRM = "Connect" + const val ONBOARDING_START = "Get started" + + /** Pixel Launcher on the google_apis emulator images: the stalls, and the tablet taskbar. */ + const val LAUNCHER = "com.google.android.apps.nexuslauncher" + const val SYSTEM_UI_SETTLE_MS = 3_000L + const val DEMO_BURSTS = 2 + + const val READ_WARM_UP_MS = 3_000L + + /** Logged by both flavors' maps (MapView.kt, MeshMap.kt) once the tiles are drawn. */ + const val MAP_DRAWN_TAG = "MapDrawn" + const val MAP_DRAWN_MESSAGE = "tiles drawn" + const val MAP_DRAWN_TIMEOUT_MS = 45_000L + const val MILLIS_PER_SECOND = 1_000L + + const val CONNECT_ATTEMPTS = 3 + const val CONNECT_TIMEOUT_MS = 60_000L + const val POLL_MS = 500L + const val PNG_QUALITY = 100 + } +}